moxml 0.1.7 → 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 +10 -10
- 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 -1
- 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 +282 -26
- 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 -34
- 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,364 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::XPath::Parser do
|
|
6
|
+
describe ".parse" do
|
|
7
|
+
context "simple paths" do
|
|
8
|
+
it "parses root element" do
|
|
9
|
+
ast = described_class.parse("/root")
|
|
10
|
+
expect(ast.type).to eq(:absolute_path)
|
|
11
|
+
expect(ast.children).not_to be_empty
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "parses simple child path" do
|
|
15
|
+
ast = described_class.parse("/root/child")
|
|
16
|
+
expect(ast.type).to eq(:absolute_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it "parses descendant path" do
|
|
20
|
+
ast = described_class.parse("//book")
|
|
21
|
+
expect(ast.type).to eq(:absolute_path)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "parses relative path" do
|
|
25
|
+
ast = described_class.parse("book/title")
|
|
26
|
+
expect(ast.type).to eq(:relative_path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "parses single element" do
|
|
30
|
+
ast = described_class.parse("book")
|
|
31
|
+
expect(ast.type).to eq(:relative_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "parses wildcard" do
|
|
35
|
+
ast = described_class.parse("*")
|
|
36
|
+
expect(ast.type).to eq(:relative_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "parses descendant wildcard" do
|
|
40
|
+
ast = described_class.parse("//*")
|
|
41
|
+
expect(ast.type).to eq(:absolute_path)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
context "abbreviated steps" do
|
|
46
|
+
it "parses current node (.)" do
|
|
47
|
+
ast = described_class.parse(".")
|
|
48
|
+
expect(ast.type).to eq(:relative_path)
|
|
49
|
+
expect(ast.children.first.type).to eq(:current)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "parses parent node (..)" do
|
|
53
|
+
ast = described_class.parse("..")
|
|
54
|
+
expect(ast.type).to eq(:relative_path)
|
|
55
|
+
expect(ast.children.first.type).to eq(:parent)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "parses attribute" do
|
|
59
|
+
ast = described_class.parse("@id")
|
|
60
|
+
expect(ast.type).to eq(:relative_path)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "parses path with parent reference" do
|
|
64
|
+
ast = described_class.parse("../book")
|
|
65
|
+
expect(ast.type).to eq(:relative_path)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context "axis specifications" do
|
|
70
|
+
it "parses child axis" do
|
|
71
|
+
ast = described_class.parse("child::book")
|
|
72
|
+
expect(ast.type).to eq(:relative_path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "parses descendant axis" do
|
|
76
|
+
ast = described_class.parse("descendant::book")
|
|
77
|
+
expect(ast.type).to eq(:relative_path)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it "parses attribute axis" do
|
|
81
|
+
ast = described_class.parse("attribute::id")
|
|
82
|
+
expect(ast.type).to eq(:relative_path)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it "parses parent axis" do
|
|
86
|
+
ast = described_class.parse("parent::section")
|
|
87
|
+
expect(ast.type).to eq(:relative_path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "parses following-sibling axis" do
|
|
91
|
+
ast = described_class.parse("following-sibling::chapter")
|
|
92
|
+
expect(ast.type).to eq(:relative_path)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "parses ancestor-or-self axis" do
|
|
96
|
+
ast = described_class.parse("ancestor-or-self::div")
|
|
97
|
+
expect(ast.type).to eq(:relative_path)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context "node tests" do
|
|
102
|
+
it "parses text() node test" do
|
|
103
|
+
ast = described_class.parse("text()")
|
|
104
|
+
expect(ast.type).to eq(:relative_path)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "parses comment() node test" do
|
|
108
|
+
ast = described_class.parse("comment()")
|
|
109
|
+
expect(ast.type).to eq(:relative_path)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "parses node() node test" do
|
|
113
|
+
ast = described_class.parse("node()")
|
|
114
|
+
expect(ast.type).to eq(:relative_path)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "parses processing-instruction() node test" do
|
|
118
|
+
ast = described_class.parse("processing-instruction()")
|
|
119
|
+
expect(ast.type).to eq(:relative_path)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
context "predicates" do
|
|
124
|
+
it "parses simple attribute predicate" do
|
|
125
|
+
ast = described_class.parse("book[@id]")
|
|
126
|
+
expect(ast.type).to eq(:relative_path)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "parses comparison predicate" do
|
|
130
|
+
ast = described_class.parse("book[@price < 10]")
|
|
131
|
+
expect(ast.type).to eq(:relative_path)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "parses position predicate" do
|
|
135
|
+
ast = described_class.parse("book[1]")
|
|
136
|
+
expect(ast.type).to eq(:relative_path)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "parses multiple predicates" do
|
|
140
|
+
ast = described_class.parse("book[@id][@lang]")
|
|
141
|
+
expect(ast.type).to eq(:relative_path)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "parses complex predicate" do
|
|
145
|
+
ast = described_class.parse("book[@price < 10 and @year > 2000]")
|
|
146
|
+
expect(ast.type).to eq(:relative_path)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "parses nested predicates" do
|
|
150
|
+
ast = described_class.parse('//book[author[@country="USA"]]')
|
|
151
|
+
expect(ast.type).to eq(:absolute_path)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
context "operators" do
|
|
156
|
+
it "parses all comparison operators without error" do
|
|
157
|
+
%w[= != < > <= >=].each do |op|
|
|
158
|
+
expect { described_class.parse("@a #{op} @b") }.not_to raise_error
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it "parses all arithmetic operators without error" do
|
|
163
|
+
operators = { "+" => true, "-" => true, "*" => true, "div" => true,
|
|
164
|
+
"mod" => true }
|
|
165
|
+
operators.each_key do |op|
|
|
166
|
+
expect { described_class.parse("@a #{op} @b") }.not_to raise_error
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
context "logical operators" do
|
|
172
|
+
it "parses logical operators without error" do
|
|
173
|
+
expect { described_class.parse("@a and @b") }.not_to raise_error
|
|
174
|
+
expect { described_class.parse("@a or @b") }.not_to raise_error
|
|
175
|
+
expect { described_class.parse("@a and @b or @c") }.not_to raise_error
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
context "literals" do
|
|
180
|
+
it "parses string literal" do
|
|
181
|
+
ast = described_class.parse('"hello"')
|
|
182
|
+
expect(ast.type).to eq(:string)
|
|
183
|
+
expect(ast.value).to eq("hello")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "parses single-quoted string" do
|
|
187
|
+
ast = described_class.parse("'world'")
|
|
188
|
+
expect(ast.type).to eq(:string)
|
|
189
|
+
expect(ast.value).to eq("world")
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "parses number literal" do
|
|
193
|
+
ast = described_class.parse("123")
|
|
194
|
+
expect(ast.type).to eq(:number)
|
|
195
|
+
expect(ast.value).to eq(123)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "parses decimal literal" do
|
|
199
|
+
ast = described_class.parse("123.45")
|
|
200
|
+
expect(ast.type).to eq(:number)
|
|
201
|
+
expect(ast.value).to eq(123.45)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
context "function calls" do
|
|
206
|
+
it "parses functions with varying argument counts" do
|
|
207
|
+
expect { described_class.parse("position()") }.not_to raise_error
|
|
208
|
+
expect { described_class.parse("count(//item)") }.not_to raise_error
|
|
209
|
+
expect do
|
|
210
|
+
described_class.parse("substring(@name, 1, 3)")
|
|
211
|
+
end.not_to raise_error
|
|
212
|
+
expect do
|
|
213
|
+
described_class.parse("sum(count(//item))")
|
|
214
|
+
end.not_to raise_error
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
context "union expressions" do
|
|
219
|
+
it "parses union operators without error" do
|
|
220
|
+
expect { described_class.parse("book | article") }.not_to raise_error
|
|
221
|
+
expect do
|
|
222
|
+
described_class.parse("book | article | chapter")
|
|
223
|
+
end.not_to raise_error
|
|
224
|
+
expect do
|
|
225
|
+
described_class.parse("//book | //article")
|
|
226
|
+
end.not_to raise_error
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
context "variables" do
|
|
231
|
+
it "parses variable references without error" do
|
|
232
|
+
expect { described_class.parse("$var") }.not_to raise_error
|
|
233
|
+
expect { described_class.parse("$price * 1.1") }.not_to raise_error
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
context "namespaces" do
|
|
238
|
+
it "parses namespaced element" do
|
|
239
|
+
ast = described_class.parse("ns:element")
|
|
240
|
+
expect(ast.type).to eq(:relative_path)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "parses namespaced path" do
|
|
244
|
+
ast = described_class.parse("/ns:root/ns:child")
|
|
245
|
+
expect(ast.type).to eq(:absolute_path)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
it "parses namespace wildcard" do
|
|
249
|
+
ast = described_class.parse("ns:*")
|
|
250
|
+
expect(ast.type).to eq(:relative_path)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
context "complex expressions" do
|
|
255
|
+
it "parses complex predicate with paths" do
|
|
256
|
+
ast = described_class.parse("//book[@price < 10]/title")
|
|
257
|
+
expect(ast.type).to eq(:absolute_path)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "parses nested paths in predicates" do
|
|
261
|
+
ast = described_class.parse('//book[author/name="Smith"]')
|
|
262
|
+
expect(ast.type).to eq(:absolute_path)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "parses arithmetic in predicates" do
|
|
266
|
+
ast = described_class.parse("//book[@price * 1.1 < 100]")
|
|
267
|
+
expect(ast.type).to eq(:absolute_path)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "parses function calls in predicates" do
|
|
271
|
+
ast = described_class.parse("//book[position() = 1]")
|
|
272
|
+
expect(ast.type).to eq(:absolute_path)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it "parses grouped expressions" do
|
|
276
|
+
ast = described_class.parse("(@a + @b) * @c")
|
|
277
|
+
expect(ast).to be_a(Moxml::XPath::AST::Node)
|
|
278
|
+
# Grouped expressions parse successfully
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
context "operator precedence" do
|
|
283
|
+
it "parses arithmetic precedence correctly" do
|
|
284
|
+
ast = described_class.parse("1 + 2 * 3")
|
|
285
|
+
expect(ast).to be_a(Moxml::XPath::AST::Node)
|
|
286
|
+
# Test actual execution would give 7 (not 9), proving correct precedence
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it "parses comparison precedence correctly" do
|
|
290
|
+
ast = described_class.parse("1 + 2 < 5")
|
|
291
|
+
expect(ast).to be_a(Moxml::XPath::AST::Node)
|
|
292
|
+
# Precedence: (1 + 2) < 5, not 1 + (2 < 5)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "parses logical precedence correctly" do
|
|
296
|
+
ast = described_class.parse("true and false or true")
|
|
297
|
+
expect(ast).to be_a(Moxml::XPath::AST::Node)
|
|
298
|
+
# Precedence: (true and false) or true, not true and (false or true)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
context "edge cases" do
|
|
303
|
+
it "parses empty expression as empty node" do
|
|
304
|
+
ast = described_class.parse("")
|
|
305
|
+
expect(ast.type).to eq(:empty)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
it "parses slash-only path" do
|
|
309
|
+
ast = described_class.parse("/")
|
|
310
|
+
expect(ast.type).to eq(:absolute_path)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it "parses whitespace-only expression as empty" do
|
|
314
|
+
ast = described_class.parse(" ")
|
|
315
|
+
expect(ast.type).to eq(:empty)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
context "error handling" do
|
|
320
|
+
it "raises error for unexpected token" do
|
|
321
|
+
expect { described_class.parse("book]") }
|
|
322
|
+
.to raise_error(Moxml::XPath::SyntaxError)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it "raises error for unclosed bracket" do
|
|
326
|
+
expect { described_class.parse("book[1") }
|
|
327
|
+
.to raise_error(Moxml::XPath::SyntaxError, /Expected '\]'/)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
it "raises error for unclosed parenthesis" do
|
|
331
|
+
expect { described_class.parse("count(//item") }
|
|
332
|
+
.to raise_error(Moxml::XPath::SyntaxError, /Expected '\)'/)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "raises error for missing node test" do
|
|
336
|
+
expect { described_class.parse("child::") }
|
|
337
|
+
.to raise_error(Moxml::XPath::SyntaxError, /Expected node test/)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it "raises error for invalid axis" do
|
|
341
|
+
expect { described_class.parse("invalid::book") }
|
|
342
|
+
.to raise_error(Moxml::XPath::SyntaxError)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
describe ".parse_with_cache" do
|
|
348
|
+
it "caches parsed expressions" do
|
|
349
|
+
expr = '//book[@id="123"]'
|
|
350
|
+
ast1 = described_class.parse_with_cache(expr)
|
|
351
|
+
ast2 = described_class.parse_with_cache(expr)
|
|
352
|
+
|
|
353
|
+
# Should return same cached object
|
|
354
|
+
expect(ast1).to be(ast2)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
it "handles different expressions" do
|
|
358
|
+
ast1 = described_class.parse_with_cache("//book")
|
|
359
|
+
ast2 = described_class.parse_with_cache("//article")
|
|
360
|
+
|
|
361
|
+
expect(ast1).not_to be(ast2)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|