lutaml-model 0.8.15 → 0.8.17
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/release.yml +0 -3
- data/.rubocop_todo.yml +49 -52
- data/README.adoc +188 -25
- data/docs/_guides/xml/namespace-semantics.adoc +2 -0
- data/docs/_guides/xml-mapping.adoc +178 -24
- data/docs/_guides/xml-namespace-qualification.adoc +142 -0
- data/docs/_pages/importable_models.adoc +7 -1
- data/docs/_tutorials/xml-element-attribute-namespace-guide.adoc +2 -0
- data/docs/_tutorials/xml-schema-primer-style-guide.adoc +2 -0
- data/docs/namespace-management.adoc +2 -0
- data/docs/xml-schema-qualification.md +6 -0
- data/lib/lutaml/jsonld.rb +1 -4
- data/lib/lutaml/model/attribute.rb +4 -0
- data/lib/lutaml/model/choice.rb +34 -0
- data/lib/lutaml/model/compiled_rule.rb +7 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +3 -1
- data/lib/lutaml/{jsonld → rdf}/context.rb +1 -1
- data/lib/lutaml/{jsonld/transform.rb → rdf/linked_data_transform.rb} +33 -35
- data/lib/lutaml/{jsonld → rdf}/term_definition.rb +1 -1
- data/lib/lutaml/rdf.rb +3 -0
- data/lib/lutaml/turtle/transform.rb +2 -1
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +3 -1
- data/lib/lutaml/xml/adapter/xml_serializer.rb +14 -4
- data/lib/lutaml/xml/adapter.rb +2 -1
- data/lib/lutaml/xml/adapter_element.rb +2 -2
- data/lib/lutaml/xml/builder/base.rb +2 -1
- data/lib/lutaml/xml/data_model.rb +19 -3
- data/lib/lutaml/xml/mapping.rb +3 -1
- data/lib/lutaml/xml/mapping_rule.rb +28 -2
- data/lib/lutaml/xml/model_transform.rb +9 -1
- data/lib/lutaml/xml/serialization/instance_methods.rb +16 -9
- data/lib/lutaml/xml/transformation/element_builder.rb +39 -26
- data/lib/lutaml/xml/transformation/rule_applier.rb +21 -0
- data/lib/lutaml/xml/transformation/rule_compiler.rb +12 -3
- data/lib/lutaml/yamlld/adapter.rb +25 -0
- data/lib/lutaml/yamlld.rb +25 -0
- data/spec/lutaml/integration/multi_format_spec.rb +23 -0
- data/spec/lutaml/model/attribute_spec.rb +8 -1
- data/spec/lutaml/model/choice_restrict_spec.rb +225 -0
- data/spec/lutaml/model/custom_model_spec.rb +4 -4
- data/spec/lutaml/model/mixed_content_spec.rb +50 -9
- data/spec/lutaml/model/multiple_mapping_spec.rb +4 -4
- data/spec/lutaml/model/ordered_content_spec.rb +3 -3
- data/spec/lutaml/model/raw_element_spec.rb +533 -0
- data/spec/lutaml/model/uninitialized_class_spec.rb +1 -1
- data/spec/lutaml/model/xsd_form_default_patterns_spec.rb +2 -2
- data/spec/lutaml/model/xsd_patterns_spec.rb +4 -4
- data/spec/lutaml/{jsonld → rdf}/context_spec.rb +2 -2
- data/spec/lutaml/{jsonld/transform_spec.rb → rdf/linked_data_transform_spec.rb} +18 -2
- data/spec/lutaml/rdf/mapping_spec.rb +2 -1
- data/spec/lutaml/{jsonld → rdf}/term_definition_spec.rb +2 -2
- data/spec/lutaml/turtle/transform_spec.rb +2 -2
- data/spec/lutaml/xml/enhanced_mapping_spec.rb +230 -1
- data/spec/lutaml/xml/mapping_spec.rb +4 -4
- data/spec/lutaml/xml/namespace_placement_spec.rb +11 -8
- data/spec/lutaml/xml/namespace_three_phase_spec.rb +1 -1
- data/spec/lutaml/xml/prefix_control_spec.rb +4 -4
- data/spec/lutaml/xml/serializable_namespace_spec.rb +6 -6
- data/spec/lutaml/xml/type_namespace_examples_spec.rb +1 -1
- data/spec/lutaml/xml/type_namespace_integration_spec.rb +3 -3
- data/spec/lutaml/yamlld/adapter_spec.rb +56 -0
- data/spec/lutaml/yamlld/registration_spec.rb +33 -0
- metadata +14 -8
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
module RawElementSpec
|
|
6
|
+
class SvgContainer < Lutaml::Model::Serializable
|
|
7
|
+
attribute :svg_data, :string
|
|
8
|
+
|
|
9
|
+
xml do
|
|
10
|
+
element "container"
|
|
11
|
+
map_element "svg", to: :svg_data, raw: :element
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class MathContainer < Lutaml::Model::Serializable
|
|
16
|
+
attribute :formula, :string
|
|
17
|
+
|
|
18
|
+
xml do
|
|
19
|
+
element "container"
|
|
20
|
+
map_element "math", to: :formula, raw: :element
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class MixedContainer < Lutaml::Model::Serializable
|
|
25
|
+
attribute :name, :string
|
|
26
|
+
attribute :embedded, :string
|
|
27
|
+
|
|
28
|
+
xml do
|
|
29
|
+
element "container"
|
|
30
|
+
map_element "name", to: :name
|
|
31
|
+
map_element "foreign", to: :embedded, raw: :element
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class NestedRawContainer < Lutaml::Model::Serializable
|
|
36
|
+
attribute :name, :string
|
|
37
|
+
attribute :embedded, :string
|
|
38
|
+
|
|
39
|
+
xml do
|
|
40
|
+
element "container"
|
|
41
|
+
map_element "name", to: :name
|
|
42
|
+
map_element "embedded", to: :embedded, raw: :element
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class MultiRawContainer < Lutaml::Model::Serializable
|
|
47
|
+
attribute :name, :string
|
|
48
|
+
attribute :svg_data, :string
|
|
49
|
+
attribute :math_data, :string
|
|
50
|
+
|
|
51
|
+
xml do
|
|
52
|
+
element "container"
|
|
53
|
+
map_element "name", to: :name
|
|
54
|
+
map_element "svg", to: :svg_data, raw: :element
|
|
55
|
+
map_element "math", to: :math_data, raw: :element
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class CollectionRawContainer < Lutaml::Model::Serializable
|
|
60
|
+
attribute :name, :string
|
|
61
|
+
attribute :fragments, :string, collection: true
|
|
62
|
+
|
|
63
|
+
xml do
|
|
64
|
+
element "container"
|
|
65
|
+
map_element "name", to: :name
|
|
66
|
+
map_element "fragment", to: :fragments, raw: :element
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class EmptyRawContainer < Lutaml::Model::Serializable
|
|
71
|
+
attribute :name, :string
|
|
72
|
+
attribute :embedded, :string
|
|
73
|
+
|
|
74
|
+
xml do
|
|
75
|
+
element "container"
|
|
76
|
+
map_element "name", to: :name
|
|
77
|
+
map_element "foreign", to: :embedded, raw: :element
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class NormalContainer < Lutaml::Model::Serializable
|
|
82
|
+
attribute :data, :string
|
|
83
|
+
|
|
84
|
+
xml do
|
|
85
|
+
element "container"
|
|
86
|
+
map_element "data", to: :data
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
RSpec.describe "map_element raw: :element" do
|
|
91
|
+
describe "deserialization" do
|
|
92
|
+
describe "foreign namespace element capture" do
|
|
93
|
+
let(:svg_xml) do
|
|
94
|
+
<<~XML
|
|
95
|
+
<container>
|
|
96
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
97
|
+
<rect x="0" y="0" width="100" height="100" fill="red"/>
|
|
98
|
+
</svg>
|
|
99
|
+
</container>
|
|
100
|
+
XML
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "captures the full SVG element including its own tags" do
|
|
104
|
+
doc = SvgContainer.from_xml(svg_xml)
|
|
105
|
+
expect(doc.svg_data).to include("<svg")
|
|
106
|
+
expect(doc.svg_data).to include("</svg>")
|
|
107
|
+
expect(doc.svg_data).to include("viewBox")
|
|
108
|
+
expect(doc.svg_data).to include("<rect")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "preserves SVG namespace attribute" do
|
|
112
|
+
doc = SvgContainer.from_xml(svg_xml)
|
|
113
|
+
expect(doc.svg_data).to include("xmlns=\"http://www.w3.org/2000/svg\"")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "preserves child elements" do
|
|
117
|
+
doc = SvgContainer.from_xml(svg_xml)
|
|
118
|
+
expect(doc.svg_data).to include("<rect")
|
|
119
|
+
expect(doc.svg_data).to include("fill=\"red\"")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe "non-namespaced element capture" do
|
|
124
|
+
let(:math_xml) do
|
|
125
|
+
"<container><math><mfrac><mi>x</mi><mn>2</mn></mfrac></math></container>"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "captures the full element" do
|
|
129
|
+
doc = MathContainer.from_xml(math_xml)
|
|
130
|
+
expect(doc.formula).to include("<math>")
|
|
131
|
+
expect(doc.formula).to include("</math>")
|
|
132
|
+
expect(doc.formula).to include("<mfrac>")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
describe "mixed mapped and raw elements" do
|
|
137
|
+
let(:mixed_xml) do
|
|
138
|
+
<<~XML
|
|
139
|
+
<container>
|
|
140
|
+
<name>test</name>
|
|
141
|
+
<foreign attr="value">
|
|
142
|
+
<child>content</child>
|
|
143
|
+
</foreign>
|
|
144
|
+
</container>
|
|
145
|
+
XML
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "parses normal elements normally" do
|
|
149
|
+
doc = MixedContainer.from_xml(mixed_xml)
|
|
150
|
+
expect(doc.name).to eq("test")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it "captures raw elements as full XML" do
|
|
154
|
+
doc = MixedContainer.from_xml(mixed_xml)
|
|
155
|
+
expect(doc.embedded).to include("<foreign")
|
|
156
|
+
expect(doc.embedded).to include("</foreign>")
|
|
157
|
+
expect(doc.embedded).to include("attr=\"value\"")
|
|
158
|
+
expect(doc.embedded).to include("<child>content</child>")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "nested XML in raw element" do
|
|
163
|
+
let(:nested_xml) do
|
|
164
|
+
<<~XML
|
|
165
|
+
<container>
|
|
166
|
+
<name>test</name>
|
|
167
|
+
<embedded>
|
|
168
|
+
<level1>
|
|
169
|
+
<level2 attr="deep">text</level2>
|
|
170
|
+
</level1>
|
|
171
|
+
</embedded>
|
|
172
|
+
</container>
|
|
173
|
+
XML
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
it "preserves full nesting" do
|
|
177
|
+
doc = NestedRawContainer.from_xml(nested_xml)
|
|
178
|
+
expect(doc.embedded).to include("<level1>")
|
|
179
|
+
expect(doc.embedded).to include("<level2 attr=\"deep\">text</level2>")
|
|
180
|
+
expect(doc.embedded).to include("</level1>")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
describe "multiple raw: :element mappings" do
|
|
185
|
+
let(:multi_xml) do
|
|
186
|
+
<<~XML
|
|
187
|
+
<container>
|
|
188
|
+
<name>mixed content</name>
|
|
189
|
+
<svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>
|
|
190
|
+
<math><mi>π</mi></math>
|
|
191
|
+
</container>
|
|
192
|
+
XML
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "captures each raw element independently" do
|
|
196
|
+
doc = MultiRawContainer.from_xml(multi_xml)
|
|
197
|
+
expect(doc.name).to eq("mixed content")
|
|
198
|
+
expect(doc.svg_data).to include("<svg")
|
|
199
|
+
expect(doc.svg_data).to include("<circle")
|
|
200
|
+
expect(doc.math_data).to include("<math>")
|
|
201
|
+
expect(doc.math_data).to include("<mi>")
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
describe "collection raw: :element" do
|
|
206
|
+
let(:collection_xml) do
|
|
207
|
+
<<~XML
|
|
208
|
+
<container>
|
|
209
|
+
<name>multi</name>
|
|
210
|
+
<fragment id="1">first</fragment>
|
|
211
|
+
<fragment id="2">second</fragment>
|
|
212
|
+
<fragment id="3"><nested>third</nested></fragment>
|
|
213
|
+
</container>
|
|
214
|
+
XML
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "captures each occurrence as a collection item" do
|
|
218
|
+
doc = CollectionRawContainer.from_xml(collection_xml)
|
|
219
|
+
expect(doc.name).to eq("multi")
|
|
220
|
+
expect(doc.fragments.length).to eq(3)
|
|
221
|
+
expect(doc.fragments[0]).to include("<fragment")
|
|
222
|
+
expect(doc.fragments[0]).to include("first")
|
|
223
|
+
expect(doc.fragments[1]).to include("second")
|
|
224
|
+
expect(doc.fragments[2]).to include("<nested>third</nested>")
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
describe "missing raw element" do
|
|
229
|
+
it "returns nil when raw element is absent" do
|
|
230
|
+
doc = SvgContainer.from_xml("<container><other>text</other></container>")
|
|
231
|
+
expect(doc.svg_data).to be_nil
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
describe "empty raw element" do
|
|
236
|
+
it "captures self-closing empty element" do
|
|
237
|
+
doc = EmptyRawContainer.from_xml("<container><name>test</name><foreign/></container>")
|
|
238
|
+
expect(doc.embedded).to include("<foreign")
|
|
239
|
+
expect(doc.embedded).to include("/")
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
describe "XML special characters" do
|
|
244
|
+
it "preserves entities in raw element content" do
|
|
245
|
+
xml = "<container><foreign>a & b < c</foreign></container>"
|
|
246
|
+
doc = MixedContainer.from_xml("<container><name>test</name>#{xml.match(/<container>(.*)<\/container>/m)[1]}</container>")
|
|
247
|
+
expect(doc.embedded).to include("<foreign")
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
describe "namespace matching" do
|
|
252
|
+
it "captures element with xmlns declaration on itself" do
|
|
253
|
+
doc = SvgContainer.from_xml(
|
|
254
|
+
'<container><svg xmlns="http://www.w3.org/2000/svg"><rect/></svg></container>',
|
|
255
|
+
)
|
|
256
|
+
expect(doc.svg_data).to include("<svg")
|
|
257
|
+
expect(doc.svg_data).to include('xmlns="http://www.w3.org/2000/svg"')
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "captures element with no namespace" do
|
|
261
|
+
doc = SvgContainer.from_xml(
|
|
262
|
+
"<container><svg><rect/></svg></container>",
|
|
263
|
+
)
|
|
264
|
+
expect(doc.svg_data).to include("<svg")
|
|
265
|
+
expect(doc.svg_data).to include("<rect")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it "captures explicitly prefixed element" do
|
|
269
|
+
doc = SvgContainer.from_xml(
|
|
270
|
+
'<container xmlns:s="http://www.w3.org/2000/svg"><s:svg><s:rect/></s:svg></container>',
|
|
271
|
+
)
|
|
272
|
+
expect(doc.svg_data).to include("<s:svg")
|
|
273
|
+
expect(doc.svg_data).to include("<s:rect")
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
describe "serialization" do
|
|
279
|
+
describe "round-trip with SVG" do
|
|
280
|
+
let(:svg_xml) do
|
|
281
|
+
<<~XML
|
|
282
|
+
<container>
|
|
283
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
284
|
+
<rect x="0" y="0" width="100" height="100" fill="red"/>
|
|
285
|
+
</svg>
|
|
286
|
+
</container>
|
|
287
|
+
XML
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "preserves SVG data through parse -> serialize -> parse" do
|
|
291
|
+
doc = SvgContainer.from_xml(svg_xml)
|
|
292
|
+
output = doc.to_xml
|
|
293
|
+
doc2 = SvgContainer.from_xml(output)
|
|
294
|
+
expect(doc2.svg_data).to include("<svg")
|
|
295
|
+
expect(doc2.svg_data).to include("</svg>")
|
|
296
|
+
expect(doc2.svg_data).to include("viewBox")
|
|
297
|
+
expect(doc2.svg_data).to include("<rect")
|
|
298
|
+
expect(doc2.svg_data).to include("xmlns=\"http://www.w3.org/2000/svg\"")
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
describe "round-trip with mixed content" do
|
|
303
|
+
let(:mixed_xml) do
|
|
304
|
+
'<container><name>test</name><foreign attr="value"><child>content</child></foreign></container>'
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it "preserves both normal and raw elements" do
|
|
308
|
+
doc = MixedContainer.from_xml(mixed_xml)
|
|
309
|
+
output = doc.to_xml
|
|
310
|
+
doc2 = MixedContainer.from_xml(output)
|
|
311
|
+
expect(doc2.name).to eq("test")
|
|
312
|
+
expect(doc2.embedded).to include("<foreign")
|
|
313
|
+
expect(doc2.embedded).to include("attr=\"value\"")
|
|
314
|
+
expect(doc2.embedded).to include("<child>content</child>")
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
describe "round-trip with nested raw XML" do
|
|
319
|
+
let(:nested_xml) do
|
|
320
|
+
'<container><name>test</name><embedded><level1><level2 attr="deep">text</level2></level1></embedded></container>'
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
it "preserves full nesting depth" do
|
|
324
|
+
doc = NestedRawContainer.from_xml(nested_xml)
|
|
325
|
+
output = doc.to_xml
|
|
326
|
+
doc2 = NestedRawContainer.from_xml(output)
|
|
327
|
+
expect(doc2.embedded).to include("<level1>")
|
|
328
|
+
expect(doc2.embedded).to include("<level2 attr=\"deep\">text</level2>")
|
|
329
|
+
expect(doc2.embedded).to include("</level1>")
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
describe "building from model instance" do
|
|
334
|
+
it "serializes a programmatically created model" do
|
|
335
|
+
model = SvgContainer.new(svg_data: '<svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>')
|
|
336
|
+
xml = model.to_xml
|
|
337
|
+
expect(xml).to include("<svg")
|
|
338
|
+
expect(xml).to include("<circle")
|
|
339
|
+
expect(xml).to include("</svg>")
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
it "round-trips a programmatically created model" do
|
|
343
|
+
original_svg = '<svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>'
|
|
344
|
+
model = SvgContainer.new(svg_data: original_svg)
|
|
345
|
+
xml = model.to_xml
|
|
346
|
+
doc = SvgContainer.from_xml(xml)
|
|
347
|
+
expect(doc.svg_data).to include("<svg")
|
|
348
|
+
expect(doc.svg_data).to include("<circle")
|
|
349
|
+
expect(doc.svg_data).to include("</svg>")
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it "does not double-escape raw element content" do
|
|
353
|
+
model = SvgContainer.new(svg_data: "<svg><rect/></svg>")
|
|
354
|
+
xml = model.to_xml
|
|
355
|
+
expect(xml).not_to include("<svg")
|
|
356
|
+
expect(xml).not_to include(">")
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
describe "round-trip with collection" do
|
|
361
|
+
let(:collection_xml) do
|
|
362
|
+
<<~XML
|
|
363
|
+
<container>
|
|
364
|
+
<name>multi</name>
|
|
365
|
+
<fragment id="1">first</fragment>
|
|
366
|
+
<fragment id="2">second</fragment>
|
|
367
|
+
</container>
|
|
368
|
+
XML
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
it "preserves all collection items" do
|
|
372
|
+
doc = CollectionRawContainer.from_xml(collection_xml)
|
|
373
|
+
output = doc.to_xml
|
|
374
|
+
doc2 = CollectionRawContainer.from_xml(output)
|
|
375
|
+
expect(doc2.fragments.length).to eq(2)
|
|
376
|
+
expect(doc2.fragments[0]).to include("first")
|
|
377
|
+
expect(doc2.fragments[1]).to include("second")
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
describe "nil and empty values" do
|
|
382
|
+
it "omits raw element when value is nil" do
|
|
383
|
+
model = SvgContainer.new(svg_data: nil)
|
|
384
|
+
xml = model.to_xml
|
|
385
|
+
expect(xml).not_to include("<svg")
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
it "omits raw element when value is empty string" do
|
|
389
|
+
model = SvgContainer.new(svg_data: "")
|
|
390
|
+
xml = model.to_xml
|
|
391
|
+
expect(xml).not_to include("<svg")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
describe "XML special characters round-trip" do
|
|
396
|
+
it "preserves angle brackets in raw element content without double-escaping" do
|
|
397
|
+
doc = MixedContainer.from_xml(
|
|
398
|
+
'<container><name>test</name><foreign><child a="1">x & y</child></foreign></container>',
|
|
399
|
+
)
|
|
400
|
+
output = doc.to_xml
|
|
401
|
+
expect(output).to include("<child")
|
|
402
|
+
expect(output).not_to include("<child")
|
|
403
|
+
doc2 = MixedContainer.from_xml(output)
|
|
404
|
+
expect(doc2.embedded).to include("<child")
|
|
405
|
+
expect(doc2.embedded).to include("x & y")
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
describe "default behavior (no raw)" do
|
|
411
|
+
it "does not capture raw XML for normal map_element" do
|
|
412
|
+
doc = NormalContainer.from_xml("<container><data>text</data></container>")
|
|
413
|
+
expect(doc.data).to eq("text")
|
|
414
|
+
expect(doc.data).not_to include("<data>")
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
it "serializes text content normally" do
|
|
418
|
+
model = NormalContainer.new(data: "hello")
|
|
419
|
+
xml = model.to_xml
|
|
420
|
+
expect(xml).to include("<data>hello</data>")
|
|
421
|
+
expect(xml).not_to include("<")
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
describe "MappingRule attributes" do
|
|
426
|
+
it "defaults raw to nil" do
|
|
427
|
+
rule = MixedContainer.mappings_for(:xml).elements.find do |r|
|
|
428
|
+
r.to == :name
|
|
429
|
+
end
|
|
430
|
+
expect(rule.raw).to be_nil
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
it "sets raw to :element when specified" do
|
|
434
|
+
rule = MixedContainer.mappings_for(:xml).elements.find do |r|
|
|
435
|
+
r.to == :embedded
|
|
436
|
+
end
|
|
437
|
+
expect(rule.raw).to eq(:element)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
it "provides raw_element backward-compat alias" do
|
|
441
|
+
rule = MixedContainer.mappings_for(:xml).elements.find do |r|
|
|
442
|
+
r.to == :embedded
|
|
443
|
+
end
|
|
444
|
+
expect(rule.raw_element?).to be(true)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
it "propagates raw through deep_dup" do
|
|
448
|
+
rule = MixedContainer.mappings_for(:xml).elements.find do |r|
|
|
449
|
+
r.to == :embedded
|
|
450
|
+
end
|
|
451
|
+
dup = rule.deep_dup
|
|
452
|
+
expect(dup.raw).to eq(:element)
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
describe "raw parameter validation" do
|
|
457
|
+
it "accepts :element" do
|
|
458
|
+
expect do
|
|
459
|
+
Lutaml::Xml::MappingRule.new("test", to: :x, raw: :element)
|
|
460
|
+
end.not_to raise_error
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
it "accepts :content" do
|
|
464
|
+
expect do
|
|
465
|
+
Lutaml::Xml::MappingRule.new("test", to: :x, raw: :content)
|
|
466
|
+
end.not_to raise_error
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
it "accepts nil" do
|
|
470
|
+
rule = Lutaml::Xml::MappingRule.new("test", to: :x, raw: nil)
|
|
471
|
+
expect(rule.raw).to be_nil
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
it "rejects invalid values" do
|
|
475
|
+
expect do
|
|
476
|
+
Lutaml::Xml::MappingRule.new("test", to: :x, raw: :invalid)
|
|
477
|
+
end.to raise_error(ArgumentError, /raw must be :element or :content/)
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
it "deprecates raw: true with warning" do
|
|
481
|
+
expect do
|
|
482
|
+
Lutaml::Xml::MappingRule.new("test", to: :x, raw: true)
|
|
483
|
+
end.to output(/DEPRECATED.*raw: :element/).to_stderr
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it "treats raw: true as :element" do
|
|
487
|
+
rule = nil
|
|
488
|
+
expect do
|
|
489
|
+
rule = Lutaml::Xml::MappingRule.new("test", to: :x, raw: true)
|
|
490
|
+
end.to output(/DEPRECATED/).to_stderr
|
|
491
|
+
expect(rule.raw).to eq(:element)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# raw: :content mode — inner XML capture
|
|
497
|
+
module RawContentSpec
|
|
498
|
+
class StreetContainer < Lutaml::Model::Serializable
|
|
499
|
+
attribute :street, :string
|
|
500
|
+
|
|
501
|
+
xml do
|
|
502
|
+
element "address"
|
|
503
|
+
map_element "street", to: :street, raw: :content
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
RSpec.describe "map_element raw: :content" do
|
|
508
|
+
describe "deserialization" do
|
|
509
|
+
it "captures inner XML content" do
|
|
510
|
+
doc = StreetContainer.from_xml("<address><street><b>123</b> Main St</street></address>")
|
|
511
|
+
expect(doc.street).to include("<b>123</b>")
|
|
512
|
+
expect(doc.street).to include("Main St")
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
it "does not include wrapper tags" do
|
|
516
|
+
doc = StreetContainer.from_xml("<address><street><b>123</b> Main St</street></address>")
|
|
517
|
+
expect(doc.street).not_to include("<street>")
|
|
518
|
+
expect(doc.street).not_to include("</street>")
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
describe "serialization" do
|
|
523
|
+
it "round-trips inner XML content" do
|
|
524
|
+
doc = StreetContainer.from_xml("<address><street><b>123</b> Main St</street></address>")
|
|
525
|
+
output = doc.to_xml
|
|
526
|
+
doc2 = StreetContainer.from_xml(output)
|
|
527
|
+
expect(doc2.street).to include("<b>123</b>")
|
|
528
|
+
expect(doc2.street).to include("Main St")
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
@@ -23,7 +23,7 @@ RSpec.describe Lutaml::Model::UninitializedClass do
|
|
|
23
23
|
|
|
24
24
|
describe "#match?" do
|
|
25
25
|
it "returns false for any argument" do
|
|
26
|
-
expect(uninitialized).not_to
|
|
26
|
+
expect(uninitialized).not_to include("pattern")
|
|
27
27
|
expect(uninitialized).not_to match "string"
|
|
28
28
|
expect(uninitialized).not_to match nil
|
|
29
29
|
end
|
|
@@ -85,7 +85,7 @@ RSpec.describe "XSD Form Default Patterns" do
|
|
|
85
85
|
xml = instance.to_xml
|
|
86
86
|
|
|
87
87
|
# The element should be unqualified (no prefix) in instance
|
|
88
|
-
expect(xml).to
|
|
88
|
+
expect(xml).to include("<premium")
|
|
89
89
|
expect(xml).to include("<elementFormUnqualified")
|
|
90
90
|
end
|
|
91
91
|
end
|
|
@@ -281,7 +281,7 @@ RSpec.describe "XSD Form Default Patterns" do
|
|
|
281
281
|
# and the namespace is already declared on an ancestor
|
|
282
282
|
# The child element should be in the blank namespace without xmlns="" declaration
|
|
283
283
|
# because the parent uses prefix format (xmlns:ex) or the namespace is already declared.
|
|
284
|
-
expect(result).not_to
|
|
284
|
+
expect(result).not_to include('xmlns=""')
|
|
285
285
|
end
|
|
286
286
|
|
|
287
287
|
it "serializes root element with namespace declaration" do
|
|
@@ -43,7 +43,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
43
43
|
|
|
44
44
|
expect(xsd).to include('<element name="item">')
|
|
45
45
|
expect(xsd).to include("<complexType>")
|
|
46
|
-
expect(xsd).not_to
|
|
46
|
+
expect(xsd).not_to include("<complexType name=")
|
|
47
47
|
expect(xsd).to include('<attribute name="id" type="xs:string"/>')
|
|
48
48
|
end
|
|
49
49
|
end
|
|
@@ -68,7 +68,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
68
68
|
expect(xsd).to include('<element name="name" type="xs:string"/>')
|
|
69
69
|
expect(xsd).to include('<element name="value" type="xs:integer"/>')
|
|
70
70
|
# Should NOT generate standalone element declaration
|
|
71
|
-
expect(xsd).not_to
|
|
71
|
+
expect(xsd).not_to include('<element name="product"')
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
it "type-only model can be referenced by other elements" do
|
|
@@ -233,7 +233,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
233
233
|
|
|
234
234
|
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
235
235
|
expect(xsd).to include('<element name="simple">')
|
|
236
|
-
expect(xsd).not_to
|
|
236
|
+
expect(xsd).not_to include('<element name="simple" type=')
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
it "Pattern 2: type_name only -> named reusable" do
|
|
@@ -252,7 +252,7 @@ RSpec.describe "XSD Three Pattern Architecture" do
|
|
|
252
252
|
xsd = Lutaml::Model::Schema.to_xsd(klass)
|
|
253
253
|
expect(xsd).to include('<complexType name="SimpleType">')
|
|
254
254
|
# Should not have standalone element declaration (only child elements in sequence are OK)
|
|
255
|
-
expect(xsd).not_to
|
|
255
|
+
expect(xsd).not_to include('<element name="SimpleType"')
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
it "Pattern 3: both -> element with named type" do
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
-
require "lutaml/
|
|
4
|
+
require "lutaml/rdf"
|
|
5
5
|
|
|
6
|
-
RSpec.describe Lutaml::
|
|
6
|
+
RSpec.describe Lutaml::Rdf::Context do
|
|
7
7
|
subject(:ctx) { described_class.new }
|
|
8
8
|
|
|
9
9
|
describe "empty context" do
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "lutaml/jsonld"
|
|
5
|
+
require "lutaml/yamlld"
|
|
5
6
|
|
|
6
|
-
RSpec.describe Lutaml::
|
|
7
|
+
RSpec.describe Lutaml::Rdf::LinkedDataTransform do
|
|
7
8
|
before do
|
|
8
9
|
stub_const("TestSkosNs", Class.new(Lutaml::Rdf::Namespace) do
|
|
9
10
|
uri "http://www.w3.org/2004/02/skos/core#"
|
|
@@ -138,6 +139,19 @@ RSpec.describe Lutaml::JsonLd::Transform do
|
|
|
138
139
|
expect(parsed["@context"]["skos"]).to eq("http://www.w3.org/2004/02/skos/core#")
|
|
139
140
|
expect(parsed["@context"]["ex"]).to eq("http://example.org/")
|
|
140
141
|
end
|
|
142
|
+
|
|
143
|
+
it "preserves data through model → YAML-LD → model" do
|
|
144
|
+
yaml = instance.to_yamlld
|
|
145
|
+
restored = JsonLdTestModel.from_yamlld(yaml)
|
|
146
|
+
expect(restored.name).to eq("test")
|
|
147
|
+
expect(restored.description).to eq("A test concept")
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it "produces the same data model from YAML-LD as JSON-LD" do
|
|
151
|
+
from_yamlld = YAML.safe_load(instance.to_yamlld)
|
|
152
|
+
from_jsonld = JSON.parse(instance.to_jsonld)
|
|
153
|
+
expect(from_yamlld).to eq(from_jsonld)
|
|
154
|
+
end
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
describe "nil attribute values" do
|
|
@@ -492,7 +506,9 @@ RSpec.describe Lutaml::JsonLd::Transform do
|
|
|
492
506
|
],
|
|
493
507
|
)
|
|
494
508
|
parsed = JSON.parse(parent.to_jsonld)
|
|
495
|
-
parent_resource = parsed["@graph"].find
|
|
509
|
+
parent_resource = parsed["@graph"].find do |r|
|
|
510
|
+
r["@type"] == "skos:Collection"
|
|
511
|
+
end
|
|
496
512
|
expect(parent_resource["member"]).to eq([
|
|
497
513
|
{ "@id" => "http://example.org/item/a" },
|
|
498
514
|
{ "@id" => "http://example.org/item/b" },
|
|
@@ -59,7 +59,8 @@ RSpec.describe Lutaml::Rdf::Mapping do
|
|
|
59
59
|
|
|
60
60
|
it "flattens nested arrays" do
|
|
61
61
|
mapping.types(["skos:Concept", "owl:Thing"], "foaf:Person")
|
|
62
|
-
expect(mapping.rdf_type).to eq(["skos:Concept", "owl:Thing",
|
|
62
|
+
expect(mapping.rdf_type).to eq(["skos:Concept", "owl:Thing",
|
|
63
|
+
"foaf:Person"])
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
it "overwrites previous types on subsequent call" do
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
|
-
require "lutaml/
|
|
4
|
+
require "lutaml/rdf"
|
|
5
5
|
|
|
6
|
-
RSpec.describe Lutaml::
|
|
6
|
+
RSpec.describe Lutaml::Rdf::TermDefinition do
|
|
7
7
|
it "simple term maps to name => id" do
|
|
8
8
|
td = described_class.new(name: "name", id: "http://example.org/name")
|
|
9
9
|
expect(td.to_context_hash).to eq("name" => "http://example.org/name")
|