moxml 0.1.6 → 0.1.8
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-repos.json +5 -0
- data/.github/workflows/dependent-tests.yml +20 -0
- data/.github/workflows/docs.yml +59 -0
- data/.github/workflows/rake.yml +12 -4
- data/.github/workflows/release.yml +5 -3
- data/.gitignore +37 -0
- data/.rubocop.yml +15 -7
- data/.rubocop_todo.yml +238 -40
- data/Gemfile +14 -9
- data/LICENSE.md +6 -2
- data/README.adoc +535 -373
- data/Rakefile +53 -0
- data/benchmarks/.gitignore +6 -0
- data/benchmarks/generate_report.rb +550 -0
- data/docs/Gemfile +13 -0
- data/docs/_config.yml +138 -0
- data/docs/_guides/advanced-features.adoc +87 -0
- data/docs/_guides/development-testing.adoc +165 -0
- data/docs/_guides/index.adoc +45 -0
- data/docs/_guides/modifying-xml.adoc +293 -0
- data/docs/_guides/parsing-xml.adoc +231 -0
- data/docs/_guides/sax-parsing.adoc +603 -0
- data/docs/_guides/working-with-documents.adoc +118 -0
- data/docs/_pages/adapter-compatibility.adoc +369 -0
- data/docs/_pages/adapters/headed-ox.adoc +237 -0
- data/docs/_pages/adapters/index.adoc +98 -0
- data/docs/_pages/adapters/libxml.adoc +286 -0
- data/docs/_pages/adapters/nokogiri.adoc +252 -0
- data/docs/_pages/adapters/oga.adoc +292 -0
- data/docs/_pages/adapters/ox.adoc +55 -0
- data/docs/_pages/adapters/rexml.adoc +293 -0
- data/docs/_pages/best-practices.adoc +430 -0
- data/docs/_pages/compatibility.adoc +468 -0
- data/docs/_pages/configuration.adoc +251 -0
- data/docs/_pages/error-handling.adoc +350 -0
- data/docs/_pages/headed-ox-limitations.adoc +558 -0
- data/docs/_pages/headed-ox.adoc +1025 -0
- data/docs/_pages/index.adoc +35 -0
- data/docs/_pages/installation.adoc +141 -0
- data/docs/_pages/node-api-reference.adoc +50 -0
- data/docs/_pages/performance.adoc +36 -0
- data/docs/_pages/quick-start.adoc +244 -0
- data/docs/_pages/thread-safety.adoc +29 -0
- data/docs/_references/document-api.adoc +408 -0
- data/docs/_references/index.adoc +48 -0
- data/docs/_tutorials/basic-usage.adoc +268 -0
- data/docs/_tutorials/builder-pattern.adoc +343 -0
- data/docs/_tutorials/index.adoc +33 -0
- data/docs/_tutorials/namespace-handling.adoc +325 -0
- data/docs/_tutorials/xpath-queries.adoc +359 -0
- data/docs/index.adoc +122 -0
- data/examples/README.md +124 -0
- data/examples/api_client/README.md +424 -0
- data/examples/api_client/api_client.rb +394 -0
- data/examples/api_client/example_response.xml +48 -0
- data/examples/headed_ox_example/README.md +90 -0
- data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
- data/examples/rss_parser/README.md +194 -0
- data/examples/rss_parser/example_feed.xml +93 -0
- data/examples/rss_parser/rss_parser.rb +189 -0
- data/examples/sax_parsing/README.md +50 -0
- data/examples/sax_parsing/data_extractor.rb +75 -0
- data/examples/sax_parsing/example.xml +21 -0
- data/examples/sax_parsing/large_file.rb +78 -0
- data/examples/sax_parsing/simple_parser.rb +55 -0
- data/examples/web_scraper/README.md +352 -0
- data/examples/web_scraper/example_page.html +201 -0
- data/examples/web_scraper/web_scraper.rb +312 -0
- data/lib/moxml/adapter/base.rb +107 -28
- data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
- data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
- data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
- data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
- data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
- data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
- data/lib/moxml/adapter/customized_ox/attribute.rb +28 -3
- data/lib/moxml/adapter/customized_ox/namespace.rb +0 -2
- data/lib/moxml/adapter/customized_ox/text.rb +0 -2
- data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
- data/lib/moxml/adapter/headed_ox.rb +161 -0
- data/lib/moxml/adapter/libxml.rb +1548 -0
- data/lib/moxml/adapter/nokogiri.rb +121 -9
- data/lib/moxml/adapter/oga.rb +123 -12
- data/lib/moxml/adapter/ox.rb +283 -27
- data/lib/moxml/adapter/rexml.rb +127 -20
- data/lib/moxml/adapter.rb +21 -4
- data/lib/moxml/attribute.rb +6 -0
- data/lib/moxml/builder.rb +40 -4
- data/lib/moxml/config.rb +8 -3
- data/lib/moxml/context.rb +39 -1
- data/lib/moxml/doctype.rb +13 -1
- data/lib/moxml/document.rb +39 -6
- data/lib/moxml/document_builder.rb +27 -5
- data/lib/moxml/element.rb +71 -2
- data/lib/moxml/error.rb +175 -6
- data/lib/moxml/node.rb +94 -3
- data/lib/moxml/node_set.rb +34 -0
- data/lib/moxml/sax/block_handler.rb +194 -0
- data/lib/moxml/sax/element_handler.rb +124 -0
- data/lib/moxml/sax/handler.rb +113 -0
- data/lib/moxml/sax.rb +31 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xml_utils/encoder.rb +4 -4
- data/lib/moxml/xml_utils.rb +7 -4
- data/lib/moxml/xpath/ast/node.rb +159 -0
- data/lib/moxml/xpath/cache.rb +91 -0
- data/lib/moxml/xpath/compiler.rb +1768 -0
- data/lib/moxml/xpath/context.rb +26 -0
- data/lib/moxml/xpath/conversion.rb +124 -0
- data/lib/moxml/xpath/engine.rb +52 -0
- data/lib/moxml/xpath/errors.rb +101 -0
- data/lib/moxml/xpath/lexer.rb +304 -0
- data/lib/moxml/xpath/parser.rb +485 -0
- data/lib/moxml/xpath/ruby/generator.rb +269 -0
- data/lib/moxml/xpath/ruby/node.rb +193 -0
- data/lib/moxml/xpath.rb +37 -0
- data/lib/moxml.rb +5 -2
- data/moxml.gemspec +3 -1
- data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
- data/spec/consistency/README.md +77 -0
- data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
- data/spec/examples/README.md +75 -0
- data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
- data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
- data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
- data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
- data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
- data/spec/integration/README.md +71 -0
- data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
- data/spec/integration/headed_ox_integration_spec.rb +326 -0
- data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
- data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
- data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
- data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
- data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
- data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
- data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
- data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
- data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
- data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
- data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
- data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
- data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
- data/spec/moxml/README.md +41 -0
- data/spec/moxml/adapter/.gitkeep +0 -0
- data/spec/moxml/adapter/README.md +61 -0
- data/spec/moxml/adapter/base_spec.rb +27 -0
- data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
- data/spec/moxml/adapter/libxml_spec.rb +14 -0
- data/spec/moxml/adapter/ox_spec.rb +9 -8
- data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
- data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
- data/spec/moxml/adapter_spec.rb +16 -0
- data/spec/moxml/attribute_spec.rb +30 -0
- data/spec/moxml/builder_spec.rb +33 -0
- data/spec/moxml/cdata_spec.rb +31 -0
- data/spec/moxml/comment_spec.rb +31 -0
- data/spec/moxml/config_spec.rb +3 -3
- data/spec/moxml/context_spec.rb +28 -0
- data/spec/moxml/declaration_spec.rb +36 -0
- data/spec/moxml/doctype_spec.rb +33 -0
- data/spec/moxml/document_builder_spec.rb +30 -0
- data/spec/moxml/document_spec.rb +105 -0
- data/spec/moxml/element_spec.rb +143 -0
- data/spec/moxml/error_spec.rb +266 -22
- data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
- data/spec/moxml/namespace_spec.rb +32 -0
- data/spec/moxml/node_set_spec.rb +39 -0
- data/spec/moxml/node_spec.rb +37 -0
- data/spec/moxml/processing_instruction_spec.rb +34 -0
- data/spec/moxml/sax_spec.rb +1067 -0
- data/spec/moxml/text_spec.rb +31 -0
- data/spec/moxml/version_spec.rb +14 -0
- data/spec/moxml/xml_utils/.gitkeep +0 -0
- data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
- data/spec/moxml/xml_utils_spec.rb +49 -0
- data/spec/moxml/xpath/ast/node_spec.rb +83 -0
- data/spec/moxml/xpath/axes_spec.rb +296 -0
- data/spec/moxml/xpath/cache_spec.rb +358 -0
- data/spec/moxml/xpath/compiler_spec.rb +406 -0
- data/spec/moxml/xpath/context_spec.rb +210 -0
- data/spec/moxml/xpath/conversion_spec.rb +365 -0
- data/spec/moxml/xpath/fixtures/sample.xml +25 -0
- data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
- data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
- data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
- data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
- data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
- data/spec/moxml/xpath/lexer_spec.rb +488 -0
- data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
- data/spec/moxml/xpath/parser_spec.rb +364 -0
- data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
- data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
- data/spec/moxml/xpath_capabilities_spec.rb +199 -0
- data/spec/moxml/xpath_spec.rb +77 -0
- data/spec/performance/README.md +83 -0
- data/spec/performance/benchmark_spec.rb +64 -0
- data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
- data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
- data/spec/performance/xpath_benchmark_spec.rb +259 -0
- data/spec/spec_helper.rb +58 -1
- data/spec/support/xml_matchers.rb +1 -1
- metadata +176 -35
- data/lib/ox/node.rb +0 -9
- data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
- /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
- /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Ensure HeadedOx adapter is loaded
|
|
6
|
+
Moxml::Adapter.load(:headed_ox)
|
|
7
|
+
|
|
8
|
+
RSpec.describe Moxml::Adapter::HeadedOx do
|
|
9
|
+
let(:adapter) { described_class }
|
|
10
|
+
let(:xml) do
|
|
11
|
+
<<~XML
|
|
12
|
+
<root>
|
|
13
|
+
<book price="10">
|
|
14
|
+
<title>Book 1</title>
|
|
15
|
+
<author>Author A</author>
|
|
16
|
+
</book>
|
|
17
|
+
<book price="20">
|
|
18
|
+
<title>Book 2</title>
|
|
19
|
+
<author>Author B</author>
|
|
20
|
+
</book>
|
|
21
|
+
<book price="30">
|
|
22
|
+
<title>Book 3</title>
|
|
23
|
+
<author>Author C</author>
|
|
24
|
+
</book>
|
|
25
|
+
</root>
|
|
26
|
+
XML
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe ".parse" do
|
|
30
|
+
it "parses XML using Ox" do
|
|
31
|
+
doc = adapter.parse(xml)
|
|
32
|
+
|
|
33
|
+
expect(doc).to be_a(Moxml::Document)
|
|
34
|
+
expect(doc.root.name).to eq("root")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe ".xpath" do
|
|
39
|
+
let(:doc) { adapter.parse(xml) }
|
|
40
|
+
|
|
41
|
+
it "executes simple XPath queries" do
|
|
42
|
+
result = adapter.xpath(doc, "/root/book")
|
|
43
|
+
|
|
44
|
+
expect(result).to be_a(Moxml::NodeSet)
|
|
45
|
+
expect(result.size).to eq(3)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "executes XPath with numeric predicates" do
|
|
49
|
+
result = adapter.xpath(doc, "//book[@price < 25]")
|
|
50
|
+
|
|
51
|
+
expect(result.size).to eq(2)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "executes XPath with string predicates" do
|
|
55
|
+
result = adapter.xpath(doc, "//book[@price='20']")
|
|
56
|
+
|
|
57
|
+
expect(result.size).to eq(1)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "executes XPath with functions" do
|
|
61
|
+
result = adapter.xpath(doc, "count(//book)")
|
|
62
|
+
|
|
63
|
+
expect(result).to eq(3.0)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "executes complex XPath with paths" do
|
|
67
|
+
result = adapter.xpath(doc, "//book[@price < 25]/title")
|
|
68
|
+
|
|
69
|
+
expect(result.size).to eq(2)
|
|
70
|
+
expect(result.map(&:text)).to contain_exactly("Book 1", "Book 2")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "supports XPath string functions in predicates" do
|
|
74
|
+
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
|
|
75
|
+
result = adapter.xpath(doc, "//book[contains(title, '2')]")
|
|
76
|
+
|
|
77
|
+
expect(result.size).to eq(1)
|
|
78
|
+
expect(result.first.xpath("title").first.text).to eq("Book 2")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "supports XPath position functions" do
|
|
82
|
+
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
|
|
83
|
+
result = adapter.xpath(doc, "//book[position() = 2]")
|
|
84
|
+
|
|
85
|
+
expect(result.size).to eq(1)
|
|
86
|
+
expect(result.first.xpath("title").first.text).to eq("Book 2")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "supports descendant axis" do
|
|
90
|
+
result = adapter.xpath(doc, "//title")
|
|
91
|
+
|
|
92
|
+
expect(result.size).to eq(3)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "supports attribute axis" do
|
|
96
|
+
result = adapter.xpath(doc, "//book/@price")
|
|
97
|
+
|
|
98
|
+
expect(result.size).to eq(3)
|
|
99
|
+
expect(result.map(&:value)).to contain_exactly("10", "20", "30")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "supports parent axis" do
|
|
103
|
+
title = adapter.xpath(doc, "//title").first
|
|
104
|
+
result = adapter.xpath(title, "parent::book")
|
|
105
|
+
|
|
106
|
+
expect(result.size).to eq(1)
|
|
107
|
+
expect(result.first.name).to eq("book")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "handles namespace queries" do
|
|
111
|
+
ns_xml = '<root xmlns:ns="http://example.com"><ns:item/></root>'
|
|
112
|
+
ns_doc = adapter.parse(ns_xml)
|
|
113
|
+
|
|
114
|
+
result = adapter.xpath(
|
|
115
|
+
ns_doc,
|
|
116
|
+
"//ns:item",
|
|
117
|
+
{ "ns" => "http://example.com" },
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(result.size).to eq(1)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "returns non-node values directly" do
|
|
124
|
+
result = adapter.xpath(doc, "string(//book[1]/title)")
|
|
125
|
+
|
|
126
|
+
expect(result).to eq("Book 1")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "handles boolean results" do
|
|
130
|
+
result = adapter.xpath(doc, "boolean(//book)")
|
|
131
|
+
|
|
132
|
+
expect(result).to be true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it "wraps error with XPathError" do
|
|
136
|
+
expect do
|
|
137
|
+
adapter.xpath(doc, "invalid[[[syntax")
|
|
138
|
+
end.to raise_error(Moxml::XPathError)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe ".at_xpath" do
|
|
143
|
+
let(:doc) { adapter.parse(xml) }
|
|
144
|
+
|
|
145
|
+
it "returns first matching node" do
|
|
146
|
+
result = adapter.at_xpath(doc, "//book")
|
|
147
|
+
|
|
148
|
+
expect(result).to be_a(Moxml::Element)
|
|
149
|
+
expect(result.name).to eq("book")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it "returns nil when no match" do
|
|
153
|
+
result = adapter.at_xpath(doc, "//nonexistent")
|
|
154
|
+
|
|
155
|
+
expect(result).to be_nil
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
it "returns scalar values directly" do
|
|
159
|
+
result = adapter.at_xpath(doc, "count(//book)")
|
|
160
|
+
|
|
161
|
+
expect(result).to eq(3.0)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
describe ".xpath_supported?" do
|
|
166
|
+
it "returns true" do
|
|
167
|
+
expect(adapter.xpath_supported?).to be true
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
describe ".capabilities" do
|
|
172
|
+
it "reports full XPath support" do
|
|
173
|
+
caps = adapter.capabilities
|
|
174
|
+
|
|
175
|
+
expect(caps[:xpath_full]).to be true
|
|
176
|
+
expect(caps[:xpath_axes]).to eq(:partial) # 6 of 13 axes supported
|
|
177
|
+
expect(caps[:xpath_functions]).to eq(:complete) # All 27 XPath 1.0 functions
|
|
178
|
+
expect(caps[:xpath_predicates]).to be true
|
|
179
|
+
expect(caps[:xpath_namespaces]).to be true
|
|
180
|
+
expect(caps[:xpath_variables]).to be true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "inherits Ox adapter capabilities" do
|
|
184
|
+
caps = adapter.capabilities
|
|
185
|
+
|
|
186
|
+
# Should have parsing capability from Ox
|
|
187
|
+
expect(caps).to have_key(:parse)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
describe "XPath function support" do
|
|
192
|
+
let(:doc) { adapter.parse(xml) }
|
|
193
|
+
|
|
194
|
+
context "string functions" do
|
|
195
|
+
it "supports string()" do
|
|
196
|
+
result = adapter.xpath(doc, "string(//book[1]/title)")
|
|
197
|
+
expect(result).to eq("Book 1")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "supports concat()" do
|
|
201
|
+
result = adapter.xpath(doc, "concat('Price: ', //book[1]/@price)")
|
|
202
|
+
expect(result).to eq("Price: 10")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it "supports contains()" do
|
|
206
|
+
result = adapter.xpath(doc, "//book[contains(title, 'Book')]")
|
|
207
|
+
expect(result.size).to eq(3)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
it "supports starts-with()" do
|
|
211
|
+
result = adapter.xpath(doc, "//book[starts-with(title, 'Book')]")
|
|
212
|
+
expect(result.size).to eq(3)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it "supports substring()" do
|
|
216
|
+
result = adapter.xpath(doc, "substring('Hello World', 7)")
|
|
217
|
+
expect(result).to eq("World")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it "supports string-length()" do
|
|
221
|
+
result = adapter.xpath(doc, "string-length('Hello')")
|
|
222
|
+
expect(result).to eq(5.0)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
it "supports normalize-space()" do
|
|
226
|
+
result = adapter.xpath(doc, "normalize-space(' hello world ')")
|
|
227
|
+
expect(result).to eq("hello world")
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
context "numeric functions" do
|
|
232
|
+
it "supports number()" do
|
|
233
|
+
result = adapter.xpath(doc, "number(//book[1]/@price)")
|
|
234
|
+
expect(result).to eq(10.0)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "supports sum()" do
|
|
238
|
+
result = adapter.xpath(doc, "sum(//book/@price)")
|
|
239
|
+
expect(result).to eq(60.0)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
it "supports count()" do
|
|
243
|
+
result = adapter.xpath(doc, "count(//book)")
|
|
244
|
+
expect(result).to eq(3.0)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it "supports floor()" do
|
|
248
|
+
result = adapter.xpath(doc, "floor(3.7)")
|
|
249
|
+
expect(result).to eq(3.0)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
it "supports ceiling()" do
|
|
253
|
+
result = adapter.xpath(doc, "ceiling(3.2)")
|
|
254
|
+
expect(result).to eq(4.0)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it "supports round()" do
|
|
258
|
+
result = adapter.xpath(doc, "round(3.5)")
|
|
259
|
+
expect(result).to eq(4.0)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
context "boolean functions" do
|
|
264
|
+
it "supports boolean()" do
|
|
265
|
+
result = adapter.xpath(doc, "boolean(//book)")
|
|
266
|
+
expect(result).to be true
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it "supports not()" do
|
|
270
|
+
result = adapter.xpath(doc, "not(false())")
|
|
271
|
+
expect(result).to be true
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it "supports true()" do
|
|
275
|
+
result = adapter.xpath(doc, "true()")
|
|
276
|
+
expect(result).to be true
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it "supports false()" do
|
|
280
|
+
result = adapter.xpath(doc, "false()")
|
|
281
|
+
expect(result).to be false
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
context "node functions" do
|
|
286
|
+
it "supports name()" do
|
|
287
|
+
result = adapter.xpath(doc, "name(//book[1])")
|
|
288
|
+
expect(result).to eq("book")
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
it "supports local-name()" do
|
|
292
|
+
result = adapter.xpath(doc, "local-name(//book[1])")
|
|
293
|
+
expect(result).to eq("book")
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
context "position functions" do
|
|
298
|
+
it "supports position()" do
|
|
299
|
+
result = adapter.xpath(doc, "//book[position() = 2]")
|
|
300
|
+
expect(result.size).to eq(1)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it "supports last()" do
|
|
304
|
+
skip "HeadedOx limitation: Text content access from nested elements needs investigation. See docs/HEADED_OX_LIMITATIONS.md"
|
|
305
|
+
result = adapter.xpath(doc, "//book[position() = last()]")
|
|
306
|
+
expect(result.size).to eq(1)
|
|
307
|
+
expect(result.first.xpath("title").first.text).to eq("Book 3")
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "libxml"
|
|
4
|
+
require "moxml/adapter/libxml"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Adapter::Libxml do
|
|
7
|
+
around do |example|
|
|
8
|
+
Moxml.with_config(:libxml, true, "UTF-8") do
|
|
9
|
+
example.run
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it_behaves_like "xml adapter"
|
|
14
|
+
end
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ox"
|
|
4
3
|
require "moxml/adapter/ox"
|
|
5
4
|
|
|
6
|
-
RSpec.describe Moxml::Adapter::Ox
|
|
7
|
-
|
|
8
|
-
Moxml.
|
|
9
|
-
|
|
10
|
-
config.strict_parsing = true
|
|
11
|
-
config.default_encoding = "UTF-8"
|
|
5
|
+
RSpec.describe Moxml::Adapter::Ox do
|
|
6
|
+
around do |example|
|
|
7
|
+
Moxml.with_config(:ox, true, "UTF-8") do
|
|
8
|
+
example.run
|
|
12
9
|
end
|
|
13
10
|
end
|
|
14
11
|
|
|
@@ -32,7 +29,9 @@ RSpec.describe Moxml::Adapter::Ox, skip: "Ox will be added later" do
|
|
|
32
29
|
end
|
|
33
30
|
|
|
34
31
|
describe "xpath support" do
|
|
35
|
-
let(:doc)
|
|
32
|
+
let(:doc) do
|
|
33
|
+
described_class.parse("<root><child id='1'>text</child></root>").native
|
|
34
|
+
end
|
|
36
35
|
|
|
37
36
|
it "supports basic element matching" do
|
|
38
37
|
nodes = described_class.xpath(doc, "//child")
|
|
@@ -41,6 +40,8 @@ RSpec.describe Moxml::Adapter::Ox, skip: "Ox will be added later" do
|
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
it "supports attribute matching" do
|
|
43
|
+
pending("Ox does not support attribute value predicates in XPath (documented limitation)")
|
|
44
|
+
|
|
44
45
|
nodes = described_class.xpath(doc, "//child[@id='1']")
|
|
45
46
|
expect(nodes.size).to eq(1)
|
|
46
47
|
expect(nodes.first.attributes["id"]).to eq("1")
|
|
File without changes
|
|
@@ -26,6 +26,9 @@ RSpec.shared_examples "xml adapter" do
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
it "handles malformed XML according to strict setting" do
|
|
29
|
+
if described_class.name.include?("Ox")
|
|
30
|
+
skip("Ox does not support non-strict parsing mode")
|
|
31
|
+
end
|
|
29
32
|
malformed = "<root><unclosed>"
|
|
30
33
|
|
|
31
34
|
expect do
|
|
@@ -164,8 +167,18 @@ RSpec.shared_examples "xml adapter" do
|
|
|
164
167
|
end
|
|
165
168
|
|
|
166
169
|
it "respects indentation settings" do
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
if described_class.name.include?("Oga")
|
|
171
|
+
pending("Oga does not support indentation settings")
|
|
172
|
+
end
|
|
173
|
+
if described_class.name.include?("Rexml")
|
|
174
|
+
pending("Postponed for Rexml till better times")
|
|
175
|
+
end
|
|
176
|
+
if described_class.name.include?("Libxml")
|
|
177
|
+
skip("LibXML serialization does not support indentation (documented limitation)")
|
|
178
|
+
end
|
|
179
|
+
if described_class.name.include?("Ox")
|
|
180
|
+
skip("Ox does not support indentation settings (documented limitation)")
|
|
181
|
+
end
|
|
169
182
|
|
|
170
183
|
unindented = described_class.serialize(doc, indent: 0)
|
|
171
184
|
indented = described_class.serialize(doc, indent: 2)
|
|
@@ -217,7 +230,9 @@ RSpec.shared_examples "xml adapter" do
|
|
|
217
230
|
end
|
|
218
231
|
|
|
219
232
|
it "preserves and correctly handles multiple namespaces" do
|
|
220
|
-
|
|
233
|
+
if described_class.name.include?("Rexml")
|
|
234
|
+
pending("Rexml does not respect ZPath namespaces")
|
|
235
|
+
end
|
|
221
236
|
# Parse original XML
|
|
222
237
|
doc = described_class.parse(xml).native
|
|
223
238
|
|
|
@@ -229,18 +244,21 @@ RSpec.shared_examples "xml adapter" do
|
|
|
229
244
|
# Test xpath with namespaces
|
|
230
245
|
namespaces = {
|
|
231
246
|
"svg" => "http://www.w3.org/2000/svg",
|
|
232
|
-
"xlink" => "http://www.w3.org/1999/xlink"
|
|
247
|
+
"xlink" => "http://www.w3.org/1999/xlink",
|
|
233
248
|
}
|
|
234
249
|
|
|
235
250
|
# Find use element and verify xlink:href attribute
|
|
236
251
|
use_elem = described_class.at_xpath(doc, "//svg:use", namespaces)
|
|
237
252
|
expect(use_elem).not_to be_nil
|
|
238
|
-
expect(described_class.get_attribute_value(use_elem,
|
|
253
|
+
expect(described_class.get_attribute_value(use_elem,
|
|
254
|
+
"xlink:href")).to eq("#myCircle")
|
|
239
255
|
|
|
240
256
|
# Verify circle element exists in defs
|
|
241
|
-
circle = described_class.at_xpath(doc, "//svg:defs/svg:circle",
|
|
257
|
+
circle = described_class.at_xpath(doc, "//svg:defs/svg:circle",
|
|
258
|
+
namespaces)
|
|
242
259
|
expect(circle).not_to be_nil
|
|
243
|
-
expect(described_class.get_attribute_value(circle,
|
|
260
|
+
expect(described_class.get_attribute_value(circle,
|
|
261
|
+
"id")).to eq("myCircle")
|
|
244
262
|
|
|
245
263
|
# Test default SVG namespace
|
|
246
264
|
text = described_class.at_xpath(doc, "//svg:text", namespaces)
|
|
@@ -252,7 +270,7 @@ RSpec.shared_examples "xml adapter" do
|
|
|
252
270
|
let(:xml) do
|
|
253
271
|
<<~XML
|
|
254
272
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
255
|
-
<rss version="2.0"#{
|
|
273
|
+
<rss version="2.0"#{' '}
|
|
256
274
|
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
257
275
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
258
276
|
xmlns:content="http://purl.org/rss/1.0/modules/content/">
|
|
@@ -270,6 +288,10 @@ RSpec.shared_examples "xml adapter" do
|
|
|
270
288
|
end
|
|
271
289
|
|
|
272
290
|
it "preserves and correctly handles multiple namespaces" do
|
|
291
|
+
if described_class.name.include?("Ox")
|
|
292
|
+
skip("Ox does not support namespace-aware XPath queries (documented limitation)")
|
|
293
|
+
end
|
|
294
|
+
|
|
273
295
|
# Parse original XML
|
|
274
296
|
doc = described_class.parse(xml).native
|
|
275
297
|
|
|
@@ -283,7 +305,7 @@ RSpec.shared_examples "xml adapter" do
|
|
|
283
305
|
namespaces = {
|
|
284
306
|
"atom" => "http://www.w3.org/2005/Atom",
|
|
285
307
|
"dc" => "http://purl.org/dc/elements/1.1/",
|
|
286
|
-
"content" => "http://purl.org/rss/1.0/modules/content/"
|
|
308
|
+
"content" => "http://purl.org/rss/1.0/modules/content/",
|
|
287
309
|
}
|
|
288
310
|
|
|
289
311
|
# Find creator using namespaced xpath
|
|
@@ -305,7 +327,7 @@ RSpec.shared_examples "xml adapter" do
|
|
|
305
327
|
let(:xml) do
|
|
306
328
|
<<~XML
|
|
307
329
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
308
|
-
<soap:Envelope#{
|
|
330
|
+
<soap:Envelope#{' '}
|
|
309
331
|
xmlns:soap="http://www.w3.org/2003/05/soap-envelope"
|
|
310
332
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
311
333
|
xmlns:ns="urn:example:namespace">
|
|
@@ -322,6 +344,10 @@ RSpec.shared_examples "xml adapter" do
|
|
|
322
344
|
end
|
|
323
345
|
|
|
324
346
|
it "preserves and correctly handles multiple namespaces" do
|
|
347
|
+
if described_class.name.include?("Ox")
|
|
348
|
+
skip("Ox does not support namespace-aware XPath queries (documented limitation)")
|
|
349
|
+
end
|
|
350
|
+
|
|
325
351
|
# Parse original XML
|
|
326
352
|
doc = described_class.parse(xml).native
|
|
327
353
|
|
|
@@ -334,7 +360,7 @@ RSpec.shared_examples "xml adapter" do
|
|
|
334
360
|
# Test xpath with namespaces
|
|
335
361
|
namespaces = {
|
|
336
362
|
"soap" => "http://www.w3.org/2003/05/soap-envelope",
|
|
337
|
-
"ns" => "urn:example:namespace"
|
|
363
|
+
"ns" => "urn:example:namespace",
|
|
338
364
|
}
|
|
339
365
|
|
|
340
366
|
# Find user ID using namespaced xpath
|
|
@@ -346,7 +372,8 @@ RSpec.shared_examples "xml adapter" do
|
|
|
346
372
|
expect(body).not_to be_nil
|
|
347
373
|
|
|
348
374
|
# Verify attribute with namespace
|
|
349
|
-
expect(described_class.get_attribute_value(user_id,
|
|
375
|
+
expect(described_class.get_attribute_value(user_id,
|
|
376
|
+
"xsi:type")).to eq("xsi:string")
|
|
350
377
|
end
|
|
351
378
|
end
|
|
352
379
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Adapter do
|
|
6
|
+
describe "adapter loading" do
|
|
7
|
+
it "provides Nokogiri adapter" do
|
|
8
|
+
expect(described_class::Nokogiri).to be_a(Class)
|
|
9
|
+
expect(described_class::Nokogiri.ancestors).to include(Moxml::Adapter::Base)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "provides Base adapter class" do
|
|
13
|
+
expect(described_class::Base).to be_a(Class)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Attribute do
|
|
6
|
+
let(:context) { Moxml.new }
|
|
7
|
+
let(:doc) { context.parse('<root id="123" class="test"/>') }
|
|
8
|
+
let(:element) { doc.root }
|
|
9
|
+
|
|
10
|
+
describe "#name" do
|
|
11
|
+
it "returns attribute name" do
|
|
12
|
+
attr = element.attributes.first
|
|
13
|
+
expect(%w[id class]).to include(attr.name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe "#value" do
|
|
18
|
+
it "returns attribute value" do
|
|
19
|
+
id_attr = element.attributes.find { |a| a.name == "id" }
|
|
20
|
+
expect(id_attr.value).to eq("123")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "#to_s" do
|
|
25
|
+
it "returns string representation" do
|
|
26
|
+
attr = element.attributes.first
|
|
27
|
+
expect(attr.to_s).to match(/\w+="\w+"/)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Builder do
|
|
6
|
+
let(:context) { Moxml.new }
|
|
7
|
+
|
|
8
|
+
describe "#build" do
|
|
9
|
+
it "builds a document with DSL" do
|
|
10
|
+
doc = described_class.new(context).build do
|
|
11
|
+
element "root" do
|
|
12
|
+
element "child" do
|
|
13
|
+
text "text"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
expect(doc).to be_a(Moxml::Document)
|
|
19
|
+
expect(doc.root.name).to eq("root")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "creates nested elements" do
|
|
23
|
+
doc = described_class.new(context).build do
|
|
24
|
+
element "parent" do
|
|
25
|
+
element "child1"
|
|
26
|
+
element "child2"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
expect(doc.root.children.length).to eq(2)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Cdata do
|
|
6
|
+
let(:context) { Moxml.new }
|
|
7
|
+
let(:doc) { context.parse("<root><![CDATA[test data]]></root>") }
|
|
8
|
+
|
|
9
|
+
describe "#content" do
|
|
10
|
+
it "returns CDATA content" do
|
|
11
|
+
cdata = doc.root.children.first
|
|
12
|
+
expect(cdata).to be_a(described_class)
|
|
13
|
+
expect(cdata.content).to eq("test data")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe "#to_xml" do
|
|
18
|
+
it "serializes to CDATA section" do
|
|
19
|
+
cdata = doc.root.children.first
|
|
20
|
+
expect(cdata.to_xml).to eq("<![CDATA[test data]]>")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "creation" do
|
|
25
|
+
it "creates a CDATA node" do
|
|
26
|
+
cdata = doc.create_cdata("new data")
|
|
27
|
+
expect(cdata).to be_a(described_class)
|
|
28
|
+
expect(cdata.content).to eq("new data")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::Comment do
|
|
6
|
+
let(:context) { Moxml.new }
|
|
7
|
+
let(:doc) { context.parse("<root><!-- test comment --></root>") }
|
|
8
|
+
|
|
9
|
+
describe "#content" do
|
|
10
|
+
it "returns comment content" do
|
|
11
|
+
comment = doc.root.children.first
|
|
12
|
+
expect(comment).to be_a(described_class)
|
|
13
|
+
expect(comment.content).to eq(" test comment ")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe "#to_xml" do
|
|
18
|
+
it "serializes to XML comment" do
|
|
19
|
+
comment = doc.root.children.first
|
|
20
|
+
expect(comment.to_xml).to eq("<!-- test comment -->")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe "creation" do
|
|
25
|
+
it "creates a comment node" do
|
|
26
|
+
comment = doc.create_comment("new comment")
|
|
27
|
+
expect(comment).to be_a(described_class)
|
|
28
|
+
expect(comment.content).to eq("new comment")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/spec/moxml/config_spec.rb
CHANGED
|
@@ -21,18 +21,18 @@ RSpec.describe Moxml::Config do
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
it "raises error for invalid adapter" do
|
|
24
|
-
expect { config.adapter = :invalid }.to raise_error(
|
|
24
|
+
expect { config.adapter = :invalid }.to raise_error(Moxml::AdapterError)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
it "requires adapter gem" do
|
|
28
28
|
expect { config.adapter = :oga }.not_to raise_error
|
|
29
29
|
|
|
30
|
-
expect(defined?(
|
|
30
|
+
expect(defined?(Oga)).to be_truthy
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
it "handles missing gems" do
|
|
34
34
|
allow(Moxml::Adapter).to receive(:require).and_raise(LoadError)
|
|
35
|
-
expect { config.adapter = :nokogiri }.to raise_error(
|
|
35
|
+
expect { config.adapter = :nokogiri }.to raise_error(Moxml::AdapterError)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|