moxml 0.1.14 → 0.1.16
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/.rubocop_todo.yml +117 -66
- data/Gemfile +1 -0
- data/README.adoc +11 -9
- data/Rakefile +34 -1
- data/TODO.remaining/1-entity-reference-adapter-support.md +157 -0
- data/TODO.remaining/2-entity-restoration-model-driven.md +169 -0
- data/TODO.remaining/3-entity-reference-test-coverage.md +170 -0
- data/TODO.remaining/4-lenient-entities-mode.md +106 -0
- data/TODO.remaining/5-fixture-integrity.md +65 -0
- data/TODO.remaining/6-ox-element-ordering-bug.md +36 -0
- data/TODO.remaining/7-headed-ox-limitations.md +95 -0
- data/TODO.remaining/8-xpath-predicate-gaps.md +68 -0
- data/TODO.remaining/9-cleanup-hygiene.md +42 -0
- data/TODO.remaining/README.md +54 -0
- data/benchmarks/generate_report.rb +1 -1
- data/docs/_pages/configuration.adoc +22 -19
- data/docs/_tutorials/namespace-handling.adoc +5 -5
- data/lib/moxml/adapter/base.rb +22 -3
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -1
- data/lib/moxml/adapter/customized_libxml/entity_reference.rb +23 -0
- data/lib/moxml/adapter/customized_libxml.rb +18 -0
- data/lib/moxml/adapter/customized_oga.rb +10 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +25 -0
- data/lib/moxml/adapter/customized_ox.rb +12 -0
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +19 -0
- data/lib/moxml/adapter/customized_rexml/formatter.rb +44 -20
- data/lib/moxml/adapter/customized_rexml.rb +11 -0
- data/lib/moxml/adapter/headed_ox.rb +37 -14
- data/lib/moxml/adapter/libxml.rb +233 -119
- data/lib/moxml/adapter/nokogiri.rb +22 -11
- data/lib/moxml/adapter/oga.rb +64 -25
- data/lib/moxml/adapter/ox.rb +198 -42
- data/lib/moxml/adapter/rexml.rb +64 -13
- data/lib/moxml/attribute.rb +3 -0
- data/lib/moxml/builder.rb +78 -24
- data/lib/moxml/config.rb +24 -7
- data/lib/moxml/declaration.rb +4 -2
- data/lib/moxml/document.rb +8 -1
- data/lib/moxml/document_builder.rb +44 -37
- data/lib/moxml/element.rb +18 -5
- data/lib/moxml/entity_registry.rb +51 -1
- data/lib/moxml/native_attachment.rb +65 -0
- data/lib/moxml/node.rb +39 -50
- data/lib/moxml/node_set.rb +43 -15
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils.rb +1 -1
- data/lib/moxml/xpath/compiler.rb +4 -1
- data/lib/moxml.rb +1 -0
- data/scripts/format_xml.rb +16 -0
- data/scripts/pretty_format_xml.rb +14 -0
- data/spec/consistency/round_trip_spec.rb +3 -30
- data/spec/integration/all_adapters_spec.rb +1 -0
- data/spec/integration/headed_ox_integration_spec.rb +0 -2
- data/spec/integration/shared_examples/edge_cases.rb +7 -4
- data/spec/integration/shared_examples/integration_workflows.rb +3 -3
- data/spec/integration/shared_examples/node_wrappers/cdata_behavior.rb +1 -1
- data/spec/integration/shared_examples/node_wrappers/entity_reference_behavior.rb +224 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +1 -1
- data/spec/moxml/adapter/headed_ox_spec.rb +8 -8
- data/spec/moxml/adapter/oga_spec.rb +46 -0
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +1 -12
- data/spec/moxml/allocation_benchmark_spec.rb +96 -0
- data/spec/moxml/allocation_guard_spec.rb +282 -0
- data/spec/moxml/builder_spec.rb +256 -0
- data/spec/moxml/config_spec.rb +11 -11
- data/spec/moxml/doctype_spec.rb +41 -0
- data/spec/moxml/lazy_parse_spec.rb +115 -0
- data/spec/moxml/namespace_uri_validation_spec.rb +11 -3
- data/spec/moxml/node_cache_spec.rb +110 -0
- data/spec/moxml/node_set_cache_spec.rb +90 -0
- data/spec/moxml/xml_utils_spec.rb +32 -0
- data/spec/moxml/xpath/axes_spec.rb +1 -1
- data/spec/moxml/xpath/compiler_spec.rb +2 -2
- data/spec/moxml/xpath/functions/position_functions_spec.rb +5 -5
- data/spec/moxml/xpath/functions/special_functions_spec.rb +1 -1
- data/spec/performance/memory_usage_spec.rb +0 -4
- data/spec/support/allocation_helper.rb +165 -0
- data/spec/support/w3c_namespace_helpers.rb +2 -1
- metadata +29 -2
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "support/allocation_helper"
|
|
5
|
+
|
|
6
|
+
# Allocation guard specs — these run in CI by default (no :performance tag).
|
|
7
|
+
#
|
|
8
|
+
# These specs enforce allocation budgets per adapter per operation.
|
|
9
|
+
# If an adapter exceeds its threshold, the spec fails with a diagnostic
|
|
10
|
+
# message showing the actual vs expected allocation count.
|
|
11
|
+
#
|
|
12
|
+
# Thresholds are in AllocationHelper::THRESHOLDS and should be tightened
|
|
13
|
+
# as optimizations are confirmed.
|
|
14
|
+
RSpec.describe "Allocation guards", order: :defined do
|
|
15
|
+
def threshold_for(adapter_name, operation)
|
|
16
|
+
AllocationHelper.threshold(adapter_name, operation)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def guard_allocations(adapter_name, operation, &block)
|
|
20
|
+
allocs = AllocationHelper.count_allocations(&block)
|
|
21
|
+
limit = threshold_for(adapter_name, operation)
|
|
22
|
+
|
|
23
|
+
if allocs > limit
|
|
24
|
+
# Generate StackProf diagnostic on failure
|
|
25
|
+
profile = AllocationHelper.profile_allocations(&block)
|
|
26
|
+
raise(<<~MSG)
|
|
27
|
+
#{adapter_name}/#{operation}: #{allocs} allocations exceeds limit of #{limit}
|
|
28
|
+
|
|
29
|
+
#{profile}
|
|
30
|
+
MSG
|
|
31
|
+
end
|
|
32
|
+
allocs
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
shared_examples "allocation guard" do |adapter_name|
|
|
36
|
+
let(:ctx) { Moxml::Context.new(adapter_name) }
|
|
37
|
+
|
|
38
|
+
# ----------------------------------------------------------------
|
|
39
|
+
# Parse allocation guard
|
|
40
|
+
# ----------------------------------------------------------------
|
|
41
|
+
describe "parse allocations" do
|
|
42
|
+
it "parses 100-element document within allocation budget" do
|
|
43
|
+
xml = generate_xml(100)
|
|
44
|
+
allocs = guard_allocations(adapter_name, :parse_100) { ctx.parse(xml) }
|
|
45
|
+
expect(allocs).to be <= threshold_for(adapter_name, :parse_100)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "parses 50-element document within allocation budget" do
|
|
49
|
+
xml = generate_xml(50)
|
|
50
|
+
allocs = guard_allocations(adapter_name, :parse_50) { ctx.parse(xml) }
|
|
51
|
+
expect(allocs).to be <= threshold_for(adapter_name, :parse_50)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "parse + root.name is allocation-efficient" do
|
|
55
|
+
xml = generate_xml(100)
|
|
56
|
+
allocs = guard_allocations(adapter_name, :parse_and_root) do
|
|
57
|
+
doc = ctx.parse(xml)
|
|
58
|
+
doc.root.name
|
|
59
|
+
end
|
|
60
|
+
expect(allocs).to be <= threshold_for(adapter_name, :parse_and_root)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------
|
|
65
|
+
# Cache hit guards — second access should allocate near-zero objects
|
|
66
|
+
# ----------------------------------------------------------------
|
|
67
|
+
describe "cache hit guards" do
|
|
68
|
+
let(:simple_xml) { "<root><a/><b/><c/></root>" }
|
|
69
|
+
let(:attr_xml) { '<root x="1" y="2" z="3"><child k="4"/></root>' }
|
|
70
|
+
|
|
71
|
+
it "children access is cached after first call" do
|
|
72
|
+
doc = ctx.parse(simple_xml)
|
|
73
|
+
root = doc.root
|
|
74
|
+
# Warm the cache
|
|
75
|
+
root.children.to_a
|
|
76
|
+
|
|
77
|
+
allocs = AllocationHelper.count_allocations { root.children.to_a }
|
|
78
|
+
expect(allocs).to be <= threshold_for(adapter_name, :cached_children_access),
|
|
79
|
+
"Second children access (#{allocs}) should allocate <= #{threshold_for(
|
|
80
|
+
adapter_name, :cached_children_access
|
|
81
|
+
)}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "attributes access is cached after first call" do
|
|
85
|
+
doc = ctx.parse(attr_xml)
|
|
86
|
+
root = doc.root
|
|
87
|
+
# Warm the cache
|
|
88
|
+
root.attributes
|
|
89
|
+
|
|
90
|
+
allocs = AllocationHelper.count_allocations { root.attributes }
|
|
91
|
+
expect(allocs).to be <= threshold_for(adapter_name, :cached_attributes_access),
|
|
92
|
+
"Second attributes access (#{allocs}) should allocate <= #{threshold_for(
|
|
93
|
+
adapter_name, :cached_attributes_access
|
|
94
|
+
)}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "NodeSet iteration is cached on second pass" do
|
|
98
|
+
xml = generate_xml(20)
|
|
99
|
+
doc = ctx.parse(xml)
|
|
100
|
+
root = doc.root
|
|
101
|
+
# Warm the cache
|
|
102
|
+
root.children.each { |_| nil }
|
|
103
|
+
|
|
104
|
+
allocs = AllocationHelper.count_allocations do
|
|
105
|
+
root.children.each do |_|
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
expect(allocs).to be <= threshold_for(adapter_name, :cached_iteration),
|
|
110
|
+
"Second NodeSet iteration (#{allocs}) should allocate <= #{threshold_for(
|
|
111
|
+
adapter_name, :cached_iteration
|
|
112
|
+
)}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# ----------------------------------------------------------------
|
|
117
|
+
# Round-trip allocation guard
|
|
118
|
+
# ----------------------------------------------------------------
|
|
119
|
+
describe "round-trip allocations" do
|
|
120
|
+
it "parse → serialize → parse stays within budget" do
|
|
121
|
+
xml = generate_xml(50)
|
|
122
|
+
allocs = guard_allocations(adapter_name, :round_trip) do
|
|
123
|
+
doc = ctx.parse(xml)
|
|
124
|
+
serialized = doc.to_xml
|
|
125
|
+
ctx.parse(serialized)
|
|
126
|
+
end
|
|
127
|
+
expect(allocs).to be <= threshold_for(adapter_name, :round_trip)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ----------------------------------------------------------------
|
|
132
|
+
# Scalability guard — allocations grow linearly, not quadratically
|
|
133
|
+
# ----------------------------------------------------------------
|
|
134
|
+
describe "scalability" do
|
|
135
|
+
it "allocation growth is linear with document size" do
|
|
136
|
+
xml_100 = generate_xml(100)
|
|
137
|
+
xml_200 = generate_xml(200)
|
|
138
|
+
|
|
139
|
+
allocs_100 = AllocationHelper.count_allocations { ctx.parse(xml_100) }
|
|
140
|
+
allocs_200 = AllocationHelper.count_allocations { ctx.parse(xml_200) }
|
|
141
|
+
|
|
142
|
+
ratio = allocs_200.to_f / allocs_100
|
|
143
|
+
max_ratio = threshold_for(adapter_name, :scalability_ratio)
|
|
144
|
+
|
|
145
|
+
expect(ratio).to be <= max_ratio,
|
|
146
|
+
"200-element parse (#{allocs_200}) vs 100-element (#{allocs_100}) = #{ratio.round(2)}x, " \
|
|
147
|
+
"expected <= #{max_ratio}x (linear growth)"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ----------------------------------------------------------------
|
|
152
|
+
# Cache invalidation guards — mutation breaks cache
|
|
153
|
+
# ----------------------------------------------------------------
|
|
154
|
+
describe "cache invalidation" do
|
|
155
|
+
it "adding a child invalidates children cache" do
|
|
156
|
+
xml = "<root><a/></root>"
|
|
157
|
+
doc = ctx.parse(xml)
|
|
158
|
+
root = doc.root
|
|
159
|
+
children_before = root.children
|
|
160
|
+
|
|
161
|
+
new_elem = ctx.parse("<b/>").root
|
|
162
|
+
root.add_child(new_elem)
|
|
163
|
+
|
|
164
|
+
children_after = root.children
|
|
165
|
+
expect(children_before).not_to equal(children_after),
|
|
166
|
+
"Children cache should be invalidated after add_child"
|
|
167
|
+
expect(children_after.size).to eq(2)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it "setting text invalidates children cache" do
|
|
171
|
+
xml = "<root><a/></root>"
|
|
172
|
+
doc = ctx.parse(xml)
|
|
173
|
+
root = doc.root
|
|
174
|
+
children_before = root.children
|
|
175
|
+
|
|
176
|
+
root.text = "replaced"
|
|
177
|
+
|
|
178
|
+
children_after = root.children
|
|
179
|
+
expect(children_before).not_to equal(children_after),
|
|
180
|
+
"Children cache should be invalidated after text="
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "setting attribute invalidates attributes cache" do
|
|
184
|
+
xml = '<root a="1"/>'
|
|
185
|
+
doc = ctx.parse(xml)
|
|
186
|
+
root = doc.root
|
|
187
|
+
attrs_before = root.attributes
|
|
188
|
+
|
|
189
|
+
root["b"] = "2"
|
|
190
|
+
|
|
191
|
+
attrs_after = root.attributes
|
|
192
|
+
expect(attrs_before).not_to equal(attrs_after),
|
|
193
|
+
"Attributes cache should be invalidated after []="
|
|
194
|
+
expect(attrs_after.size).to eq(2)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "removing attribute invalidates attributes cache" do
|
|
198
|
+
xml = '<root a="1" b="2"/>'
|
|
199
|
+
doc = ctx.parse(xml)
|
|
200
|
+
root = doc.root
|
|
201
|
+
attrs_before = root.attributes
|
|
202
|
+
|
|
203
|
+
root.remove_attribute("a")
|
|
204
|
+
|
|
205
|
+
attrs_after = root.attributes
|
|
206
|
+
expect(attrs_before).not_to equal(attrs_after),
|
|
207
|
+
"Attributes cache should be invalidated after remove_attribute"
|
|
208
|
+
expect(attrs_after.size).to eq(1)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "removing a child invalidates parent's children cache" do
|
|
212
|
+
xml = "<root><a/><b/></root>"
|
|
213
|
+
doc = ctx.parse(xml)
|
|
214
|
+
root = doc.root
|
|
215
|
+
children_before = root.children
|
|
216
|
+
child_a = root.children.first
|
|
217
|
+
|
|
218
|
+
child_a.remove
|
|
219
|
+
|
|
220
|
+
children_after = root.children
|
|
221
|
+
expect(children_before).not_to equal(children_after),
|
|
222
|
+
"Parent's children cache should be invalidated after child.remove"
|
|
223
|
+
expect(children_after.size).to eq(1)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ----------------------------------------------------------------
|
|
228
|
+
# NodeSet wrap cache guards
|
|
229
|
+
# ----------------------------------------------------------------
|
|
230
|
+
describe "NodeSet wrap cache" do
|
|
231
|
+
it "returns the same wrapper for same index" do
|
|
232
|
+
doc = ctx.parse("<root><a/><b/><c/></root>")
|
|
233
|
+
children = doc.root.children
|
|
234
|
+
|
|
235
|
+
first_access = children[0]
|
|
236
|
+
second_access = children[0]
|
|
237
|
+
expect(first_access).to equal(second_access),
|
|
238
|
+
"Same index should return identical wrapper object"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
it "returns the same wrapper from each as from []" do
|
|
242
|
+
doc = ctx.parse("<root><a/><b/><c/></root>")
|
|
243
|
+
children = doc.root.children
|
|
244
|
+
|
|
245
|
+
from_each = nil
|
|
246
|
+
children.each { |c| from_each = c if c.name == "b" }
|
|
247
|
+
from_index = children[1]
|
|
248
|
+
|
|
249
|
+
expect(from_each).to equal(from_index),
|
|
250
|
+
"Node from #each should be identical to same index from #[]"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
it "preserves cache across multiple iterations" do
|
|
254
|
+
doc = ctx.parse("<root><a/><b/><c/></root>")
|
|
255
|
+
children = doc.root.children
|
|
256
|
+
|
|
257
|
+
pass1 = children.map(&:name)
|
|
258
|
+
pass2 = children.map(&:name)
|
|
259
|
+
|
|
260
|
+
expect(pass1).to eq(pass2)
|
|
261
|
+
# Also verify object identity between passes
|
|
262
|
+
pass1_nodes = children.to_a
|
|
263
|
+
pass2_nodes = children.to_a
|
|
264
|
+
pass1_nodes.each_with_index do |node, i|
|
|
265
|
+
expect(node).to equal(pass2_nodes[i]),
|
|
266
|
+
"Node #{i} should be identical across iterations"
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Run guards for each adapter
|
|
273
|
+
AllocationHelper::GUARDED_ADAPTERS.each do |adapter_name|
|
|
274
|
+
describe "#{adapter_name} adapter" do
|
|
275
|
+
before(:all) do
|
|
276
|
+
skip("#{adapter_name} adapter not available") unless AllocationHelper.adapter_available?(adapter_name)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it_behaves_like "allocation guard", adapter_name
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
data/spec/moxml/builder_spec.rb
CHANGED
|
@@ -5,6 +5,28 @@ require "spec_helper"
|
|
|
5
5
|
RSpec.describe Moxml::Builder do
|
|
6
6
|
let(:context) { Moxml.new }
|
|
7
7
|
|
|
8
|
+
describe "#document" do
|
|
9
|
+
it "exposes the underlying document via document accessor" do
|
|
10
|
+
builder = described_class.new(context)
|
|
11
|
+
expect(builder.document).to be_a(Moxml::Document)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "exposes the underlying document via doc alias" do
|
|
15
|
+
builder = described_class.new(context)
|
|
16
|
+
expect(builder.doc).to be_a(Moxml::Document)
|
|
17
|
+
expect(builder.doc).to equal(builder.document)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "returns the same document before and after build" do
|
|
21
|
+
builder = described_class.new(context)
|
|
22
|
+
doc_before = builder.document
|
|
23
|
+
builder.build do
|
|
24
|
+
element "root"
|
|
25
|
+
end
|
|
26
|
+
expect(builder.document).to equal(doc_before)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
8
30
|
describe "#build" do
|
|
9
31
|
it "builds a document with DSL" do
|
|
10
32
|
doc = described_class.new(context).build do
|
|
@@ -30,4 +52,238 @@ RSpec.describe Moxml::Builder do
|
|
|
30
52
|
expect(doc.root.children.length).to eq(2)
|
|
31
53
|
end
|
|
32
54
|
end
|
|
55
|
+
|
|
56
|
+
describe "#element with no arguments" do
|
|
57
|
+
it "raises ArgumentError" do
|
|
58
|
+
builder = described_class.new(context)
|
|
59
|
+
expect do
|
|
60
|
+
builder.build { element }
|
|
61
|
+
end.to raise_error(ArgumentError, "element requires a tag name")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe "#element with Hash (tag name collision)" do
|
|
66
|
+
it "creates an <element> tag when called with a Hash" do
|
|
67
|
+
doc = described_class.new(context).build do
|
|
68
|
+
element(name: "foo", type: "bar")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
el = doc.root
|
|
72
|
+
expect(el.name).to eq("element")
|
|
73
|
+
expect(el["name"]).to eq("foo")
|
|
74
|
+
expect(el["type"]).to eq("bar")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "method_missing DSL" do
|
|
79
|
+
it "creates elements via bare method calls inside build block" do
|
|
80
|
+
doc = described_class.new(context).build do
|
|
81
|
+
root do
|
|
82
|
+
title("Hello")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
expect(doc.root.name).to eq("root")
|
|
87
|
+
expect(doc.root.children.first.name).to eq("title")
|
|
88
|
+
expect(doc.root.children.first.text).to eq("Hello")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "creates elements via explicit receiver" do
|
|
92
|
+
builder = described_class.new(context)
|
|
93
|
+
doc = builder.build do
|
|
94
|
+
element "root" do
|
|
95
|
+
builder.title("Hello")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
title_el = doc.root.children.first
|
|
100
|
+
expect(title_el.name).to eq("title")
|
|
101
|
+
expect(title_el.text).to eq("Hello")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "creates elements with attributes" do
|
|
105
|
+
builder = described_class.new(context)
|
|
106
|
+
doc = builder.build do
|
|
107
|
+
element "root" do
|
|
108
|
+
builder.item(id: "1", class: "active")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
item_el = doc.root.children.first
|
|
113
|
+
expect(item_el.name).to eq("item")
|
|
114
|
+
expect(item_el["id"]).to eq("1")
|
|
115
|
+
expect(item_el["class"]).to eq("active")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "creates elements with both text content and attributes" do
|
|
119
|
+
builder = described_class.new(context)
|
|
120
|
+
doc = builder.build do
|
|
121
|
+
element "root" do
|
|
122
|
+
builder.title("Hello", lang: "en")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
title_el = doc.root.children.first
|
|
127
|
+
expect(title_el.name).to eq("title")
|
|
128
|
+
expect(title_el.text).to eq("Hello")
|
|
129
|
+
expect(title_el["lang"]).to eq("en")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "creates nested elements with blocks" do
|
|
133
|
+
builder = described_class.new(context)
|
|
134
|
+
doc = builder.build do
|
|
135
|
+
element "root" do
|
|
136
|
+
builder.parent do
|
|
137
|
+
builder.child("text")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
parent_el = doc.root.children.first
|
|
143
|
+
expect(parent_el.name).to eq("parent")
|
|
144
|
+
child_el = parent_el.children.first
|
|
145
|
+
expect(child_el.name).to eq("child")
|
|
146
|
+
expect(child_el.text).to eq("text")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "creates elements with namespace attributes" do
|
|
150
|
+
builder = described_class.new(context)
|
|
151
|
+
doc = builder.build do
|
|
152
|
+
element "root" do
|
|
153
|
+
builder.schema(xmlns: "http://www.w3.org/2001/XMLSchema")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
schema_el = doc.root.children.first
|
|
158
|
+
expect(schema_el.name).to eq("schema")
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "trailing underscore stripping (special tags)" do
|
|
163
|
+
it "strips trailing underscore to allow reserved method names as tags" do
|
|
164
|
+
builder = described_class.new(context)
|
|
165
|
+
doc = builder.build do
|
|
166
|
+
element "root" do
|
|
167
|
+
builder.type_("Object")
|
|
168
|
+
builder.class_("String")
|
|
169
|
+
builder.id_("42")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
children = doc.root.children
|
|
174
|
+
expect(children[0].name).to eq("type")
|
|
175
|
+
expect(children[0].text).to eq("Object")
|
|
176
|
+
expect(children[1].name).to eq("class")
|
|
177
|
+
expect(children[1].text).to eq("String")
|
|
178
|
+
expect(children[2].name).to eq("id")
|
|
179
|
+
expect(children[2].text).to eq("42")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "strips trailing underscore with attributes" do
|
|
183
|
+
builder = described_class.new(context)
|
|
184
|
+
doc = builder.build do
|
|
185
|
+
element "root" do
|
|
186
|
+
builder.type_(name: "foo")
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
el = doc.root.children.first
|
|
191
|
+
expect(el.name).to eq("type")
|
|
192
|
+
expect(el["name"]).to eq("foo")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "strips trailing underscore with block" do
|
|
196
|
+
builder = described_class.new(context)
|
|
197
|
+
doc = builder.build do
|
|
198
|
+
element "root" do
|
|
199
|
+
builder.class_ do
|
|
200
|
+
builder.name_("MyClass")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
class_el = doc.root.children.first
|
|
206
|
+
expect(class_el.name).to eq("class")
|
|
207
|
+
expect(class_el.children.first.name).to eq("name")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "works with bare method calls inside build block" do
|
|
211
|
+
doc = described_class.new(context).build do
|
|
212
|
+
root do
|
|
213
|
+
type_("Object")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
expect(doc.root.children.first.name).to eq("type")
|
|
218
|
+
expect(doc.root.children.first.text).to eq("Object")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it "does not strip underscore from middle of name" do
|
|
222
|
+
builder = described_class.new(context)
|
|
223
|
+
doc = builder.build do
|
|
224
|
+
element "root" do
|
|
225
|
+
builder.my_element("text")
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
expect(doc.root.children.first.name).to eq("my_element")
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
describe "respond_to_missing?" do
|
|
234
|
+
it "returns true for arbitrary element names" do
|
|
235
|
+
builder = described_class.new(context)
|
|
236
|
+
expect(builder.respond_to?(:schema)).to be true
|
|
237
|
+
expect(builder.respond_to?(:custom_element)).to be true
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it "returns false for reserved Ruby conversion and protocol methods" do
|
|
241
|
+
builder = described_class.new(context)
|
|
242
|
+
expect(builder.respond_to?(:to_ary)).to be false
|
|
243
|
+
expect(builder.respond_to?(:to_hash)).to be false
|
|
244
|
+
expect(builder.respond_to?(:to_str)).to be false
|
|
245
|
+
expect(builder.respond_to?(:to_xml)).to be false
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
describe "method_missing argument validation" do
|
|
250
|
+
it "raises ArgumentError for unexpected extra arguments" do
|
|
251
|
+
builder = described_class.new(context)
|
|
252
|
+
expect do
|
|
253
|
+
builder.build do
|
|
254
|
+
element "root" do
|
|
255
|
+
builder.foo("text", { id: "1" }, "extra")
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end.to raise_error(ArgumentError, /unexpected arguments for foo/)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it "raises ArgumentError when combining text content with a block" do
|
|
262
|
+
builder = described_class.new(context)
|
|
263
|
+
expect do
|
|
264
|
+
builder.build do
|
|
265
|
+
element "root" do
|
|
266
|
+
builder.title("Hello") { builder.child }
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end.to raise_error(ArgumentError, /title: cannot combine text content with a block/)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
describe "@current restoration on error" do
|
|
274
|
+
it "restores @current when block raises" do
|
|
275
|
+
builder = described_class.new(context)
|
|
276
|
+
expect do
|
|
277
|
+
builder.build do
|
|
278
|
+
element "root" do
|
|
279
|
+
element "child" do
|
|
280
|
+
raise "test error"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end.to raise_error(RuntimeError, "test error")
|
|
285
|
+
|
|
286
|
+
expect(builder.instance_variable_get(:@current)).to equal(builder.document)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
33
289
|
end
|
data/spec/moxml/config_spec.rb
CHANGED
|
@@ -17,8 +17,8 @@ RSpec.describe Moxml::Config do
|
|
|
17
17
|
expect(config.entity_load_mode).to eq(:required)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
it "sets default
|
|
21
|
-
expect(config.
|
|
20
|
+
it "sets default namespace_validation_mode to :strict" do
|
|
21
|
+
expect(config.namespace_validation_mode).to eq(:strict)
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -66,26 +66,26 @@ RSpec.describe Moxml::Config do
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
-
describe "#
|
|
69
|
+
describe "#namespace_validation_mode=" do
|
|
70
70
|
it "accepts :strict" do
|
|
71
|
-
config.
|
|
72
|
-
expect(config.
|
|
71
|
+
config.namespace_validation_mode = :strict
|
|
72
|
+
expect(config.namespace_validation_mode).to eq(:strict)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
it "accepts :lenient" do
|
|
76
|
-
config.
|
|
77
|
-
expect(config.
|
|
76
|
+
config.namespace_validation_mode = :lenient
|
|
77
|
+
expect(config.namespace_validation_mode).to eq(:lenient)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
it "accepts string values" do
|
|
81
|
-
config.
|
|
82
|
-
expect(config.
|
|
81
|
+
config.namespace_validation_mode = "lenient"
|
|
82
|
+
expect(config.namespace_validation_mode).to eq(:lenient)
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
it "raises error for invalid mode" do
|
|
86
86
|
expect do
|
|
87
|
-
config.
|
|
88
|
-
end.to raise_error(ArgumentError, /Invalid
|
|
87
|
+
config.namespace_validation_mode = :invalid
|
|
88
|
+
end.to raise_error(ArgumentError, /Invalid namespace_validation_mode/)
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
data/spec/moxml/doctype_spec.rb
CHANGED
|
@@ -46,4 +46,45 @@ RSpec.describe Moxml::Doctype do
|
|
|
46
46
|
expect(doctype.name).to eq("html")
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
describe "parsing" do
|
|
51
|
+
%i[nokogiri oga rexml ox].each do |adapter_name|
|
|
52
|
+
context "with #{adapter_name} adapter" do
|
|
53
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
54
|
+
|
|
55
|
+
it "parses PUBLIC doctype with external and system identifiers" do
|
|
56
|
+
xml = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html/>'
|
|
57
|
+
doc = ctx.parse(xml)
|
|
58
|
+
doctype = doc.children.find { |c| c.is_a?(described_class) }
|
|
59
|
+
|
|
60
|
+
expect(doctype).not_to be_nil
|
|
61
|
+
expect(doctype.name).to eq("html")
|
|
62
|
+
expect(doctype.external_id).to eq("-//W3C//DTD XHTML 1.0 Strict//EN")
|
|
63
|
+
expect(doctype.system_id).to eq("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "parses SYSTEM doctype with system identifier only" do
|
|
67
|
+
xml = '<!DOCTYPE config SYSTEM "config.dtd"><config/>'
|
|
68
|
+
doc = ctx.parse(xml)
|
|
69
|
+
doctype = doc.children.find { |c| c.is_a?(described_class) }
|
|
70
|
+
|
|
71
|
+
expect(doctype).not_to be_nil
|
|
72
|
+
expect(doctype.name).to eq("config")
|
|
73
|
+
expect(doctype.external_id).to be_nil
|
|
74
|
+
expect(doctype.system_id).to eq("config.dtd")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it "parses simple doctype without identifiers" do
|
|
78
|
+
xml = "<!DOCTYPE html><html/>"
|
|
79
|
+
doc = ctx.parse(xml)
|
|
80
|
+
doctype = doc.children.find { |c| c.is_a?(described_class) }
|
|
81
|
+
|
|
82
|
+
expect(doctype).not_to be_nil
|
|
83
|
+
expect(doctype.name).to eq("html")
|
|
84
|
+
expect(doctype.external_id).to be_nil
|
|
85
|
+
expect(doctype.system_id).to be_nil
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
49
90
|
end
|