moxml 0.1.7 → 0.1.9

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.
Files changed (215) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +5 -0
  3. data/.github/workflows/dependent-tests.yml +20 -0
  4. data/.github/workflows/docs.yml +59 -0
  5. data/.github/workflows/rake.yml +10 -10
  6. data/.github/workflows/release.yml +5 -3
  7. data/.gitignore +37 -0
  8. data/.rubocop.yml +15 -7
  9. data/.rubocop_todo.yml +224 -43
  10. data/Gemfile +14 -9
  11. data/LICENSE.md +6 -2
  12. data/README.adoc +535 -373
  13. data/Rakefile +53 -0
  14. data/benchmarks/.gitignore +6 -0
  15. data/benchmarks/generate_report.rb +550 -0
  16. data/docs/Gemfile +13 -0
  17. data/docs/_config.yml +138 -0
  18. data/docs/_guides/advanced-features.adoc +87 -0
  19. data/docs/_guides/development-testing.adoc +165 -0
  20. data/docs/_guides/index.adoc +51 -0
  21. data/docs/_guides/modifying-xml.adoc +292 -0
  22. data/docs/_guides/parsing-xml.adoc +230 -0
  23. data/docs/_guides/sax-parsing.adoc +603 -0
  24. data/docs/_guides/working-with-documents.adoc +118 -0
  25. data/docs/_guides/xml-declaration.adoc +450 -0
  26. data/docs/_pages/adapter-compatibility.adoc +369 -0
  27. data/docs/_pages/adapters/headed-ox.adoc +237 -0
  28. data/docs/_pages/adapters/index.adoc +97 -0
  29. data/docs/_pages/adapters/libxml.adoc +285 -0
  30. data/docs/_pages/adapters/nokogiri.adoc +251 -0
  31. data/docs/_pages/adapters/oga.adoc +291 -0
  32. data/docs/_pages/adapters/ox.adoc +56 -0
  33. data/docs/_pages/adapters/rexml.adoc +292 -0
  34. data/docs/_pages/best-practices.adoc +429 -0
  35. data/docs/_pages/compatibility.adoc +467 -0
  36. data/docs/_pages/configuration.adoc +250 -0
  37. data/docs/_pages/error-handling.adoc +349 -0
  38. data/docs/_pages/headed-ox-limitations.adoc +574 -0
  39. data/docs/_pages/headed-ox.adoc +1025 -0
  40. data/docs/_pages/index.adoc +35 -0
  41. data/docs/_pages/installation.adoc +140 -0
  42. data/docs/_pages/node-api-reference.adoc +49 -0
  43. data/docs/_pages/performance.adoc +35 -0
  44. data/docs/_pages/quick-start.adoc +243 -0
  45. data/docs/_pages/thread-safety.adoc +28 -0
  46. data/docs/_references/document-api.adoc +407 -0
  47. data/docs/_references/index.adoc +48 -0
  48. data/docs/_tutorials/basic-usage.adoc +267 -0
  49. data/docs/_tutorials/builder-pattern.adoc +342 -0
  50. data/docs/_tutorials/index.adoc +33 -0
  51. data/docs/_tutorials/namespace-handling.adoc +324 -0
  52. data/docs/_tutorials/xpath-queries.adoc +358 -0
  53. data/docs/index.adoc +122 -0
  54. data/examples/README.md +124 -0
  55. data/examples/api_client/README.md +424 -0
  56. data/examples/api_client/api_client.rb +394 -0
  57. data/examples/api_client/example_response.xml +48 -0
  58. data/examples/headed_ox_example/README.md +90 -0
  59. data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
  60. data/examples/rss_parser/README.md +194 -0
  61. data/examples/rss_parser/example_feed.xml +93 -0
  62. data/examples/rss_parser/rss_parser.rb +189 -0
  63. data/examples/sax_parsing/README.md +50 -0
  64. data/examples/sax_parsing/data_extractor.rb +75 -0
  65. data/examples/sax_parsing/example.xml +21 -0
  66. data/examples/sax_parsing/large_file.rb +78 -0
  67. data/examples/sax_parsing/simple_parser.rb +55 -0
  68. data/examples/web_scraper/README.md +352 -0
  69. data/examples/web_scraper/example_page.html +201 -0
  70. data/examples/web_scraper/web_scraper.rb +312 -0
  71. data/lib/moxml/adapter/base.rb +107 -28
  72. data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
  73. data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
  74. data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
  75. data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
  76. data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
  77. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
  78. data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
  79. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  80. data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
  81. data/lib/moxml/adapter/customized_rexml/formatter.rb +13 -8
  82. data/lib/moxml/adapter/headed_ox.rb +161 -0
  83. data/lib/moxml/adapter/libxml.rb +1564 -0
  84. data/lib/moxml/adapter/nokogiri.rb +156 -9
  85. data/lib/moxml/adapter/oga.rb +190 -15
  86. data/lib/moxml/adapter/ox.rb +322 -28
  87. data/lib/moxml/adapter/rexml.rb +157 -28
  88. data/lib/moxml/adapter.rb +21 -4
  89. data/lib/moxml/attribute.rb +6 -0
  90. data/lib/moxml/builder.rb +40 -4
  91. data/lib/moxml/config.rb +8 -3
  92. data/lib/moxml/context.rb +57 -2
  93. data/lib/moxml/declaration.rb +9 -0
  94. data/lib/moxml/doctype.rb +13 -1
  95. data/lib/moxml/document.rb +53 -6
  96. data/lib/moxml/document_builder.rb +34 -5
  97. data/lib/moxml/element.rb +71 -2
  98. data/lib/moxml/error.rb +175 -6
  99. data/lib/moxml/node.rb +155 -4
  100. data/lib/moxml/node_set.rb +34 -0
  101. data/lib/moxml/sax/block_handler.rb +194 -0
  102. data/lib/moxml/sax/element_handler.rb +124 -0
  103. data/lib/moxml/sax/handler.rb +113 -0
  104. data/lib/moxml/sax.rb +31 -0
  105. data/lib/moxml/version.rb +1 -1
  106. data/lib/moxml/xml_utils/encoder.rb +4 -4
  107. data/lib/moxml/xml_utils.rb +7 -4
  108. data/lib/moxml/xpath/ast/node.rb +159 -0
  109. data/lib/moxml/xpath/cache.rb +91 -0
  110. data/lib/moxml/xpath/compiler.rb +1770 -0
  111. data/lib/moxml/xpath/context.rb +26 -0
  112. data/lib/moxml/xpath/conversion.rb +124 -0
  113. data/lib/moxml/xpath/engine.rb +52 -0
  114. data/lib/moxml/xpath/errors.rb +101 -0
  115. data/lib/moxml/xpath/lexer.rb +304 -0
  116. data/lib/moxml/xpath/parser.rb +485 -0
  117. data/lib/moxml/xpath/ruby/generator.rb +269 -0
  118. data/lib/moxml/xpath/ruby/node.rb +193 -0
  119. data/lib/moxml/xpath.rb +37 -0
  120. data/lib/moxml.rb +5 -2
  121. data/moxml.gemspec +3 -1
  122. data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
  123. data/spec/consistency/README.md +77 -0
  124. data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
  125. data/spec/examples/README.md +75 -0
  126. data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
  127. data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
  128. data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
  129. data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
  130. data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
  131. data/spec/integration/README.md +71 -0
  132. data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
  133. data/spec/integration/headed_ox_integration_spec.rb +326 -0
  134. data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
  135. data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
  136. data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
  137. data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
  138. data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
  139. data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
  140. data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
  141. data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -5
  142. data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
  143. data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
  144. data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
  145. data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
  146. data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
  147. data/spec/moxml/README.md +41 -0
  148. data/spec/moxml/adapter/.gitkeep +0 -0
  149. data/spec/moxml/adapter/README.md +61 -0
  150. data/spec/moxml/adapter/base_spec.rb +27 -0
  151. data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
  152. data/spec/moxml/adapter/libxml_spec.rb +14 -0
  153. data/spec/moxml/adapter/ox_spec.rb +9 -8
  154. data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
  155. data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
  156. data/spec/moxml/adapter_spec.rb +16 -0
  157. data/spec/moxml/attribute_spec.rb +30 -0
  158. data/spec/moxml/builder_spec.rb +33 -0
  159. data/spec/moxml/cdata_spec.rb +31 -0
  160. data/spec/moxml/comment_spec.rb +31 -0
  161. data/spec/moxml/config_spec.rb +3 -3
  162. data/spec/moxml/context_spec.rb +28 -0
  163. data/spec/moxml/declaration_preservation_spec.rb +217 -0
  164. data/spec/moxml/declaration_spec.rb +36 -0
  165. data/spec/moxml/doctype_spec.rb +33 -0
  166. data/spec/moxml/document_builder_spec.rb +30 -0
  167. data/spec/moxml/document_spec.rb +105 -0
  168. data/spec/moxml/element_spec.rb +143 -0
  169. data/spec/moxml/error_spec.rb +266 -22
  170. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  171. data/spec/moxml/namespace_spec.rb +32 -0
  172. data/spec/moxml/node_set_spec.rb +39 -0
  173. data/spec/moxml/node_spec.rb +37 -0
  174. data/spec/moxml/processing_instruction_spec.rb +34 -0
  175. data/spec/moxml/sax_spec.rb +1067 -0
  176. data/spec/moxml/text_spec.rb +31 -0
  177. data/spec/moxml/version_spec.rb +14 -0
  178. data/spec/moxml/xml_utils/.gitkeep +0 -0
  179. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  180. data/spec/moxml/xml_utils_spec.rb +49 -0
  181. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  182. data/spec/moxml/xpath/axes_spec.rb +296 -0
  183. data/spec/moxml/xpath/cache_spec.rb +358 -0
  184. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  185. data/spec/moxml/xpath/context_spec.rb +210 -0
  186. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  187. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  188. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  189. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  190. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  191. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  192. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  193. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  194. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  195. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  196. data/spec/moxml/xpath/parser_spec.rb +364 -0
  197. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  198. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  199. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  200. data/spec/moxml/xpath_spec.rb +77 -0
  201. data/spec/performance/README.md +83 -0
  202. data/spec/performance/benchmark_spec.rb +64 -0
  203. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +4 -1
  204. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  205. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  206. data/spec/spec_helper.rb +58 -1
  207. data/spec/support/xml_matchers.rb +1 -1
  208. metadata +178 -34
  209. data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
  210. /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
  211. /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
  212. /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
  213. /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
  214. /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
  215. /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
@@ -0,0 +1,406 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::Compiler do
6
+ let(:context) { Moxml.new(:nokogiri) }
7
+ let(:xml) do
8
+ <<~XML
9
+ <root>
10
+ <child id="1">text1</child>
11
+ <child id="2">text2</child>
12
+ <other>other text</other>
13
+ </root>
14
+ XML
15
+ end
16
+ let(:doc) { context.parse(xml) }
17
+
18
+ describe ".compile_with_cache" do
19
+ it "compiles a simple path expression" do
20
+ ast = Moxml::XPath::Parser.parse("/root")
21
+ proc = described_class.compile_with_cache(ast)
22
+
23
+ expect(proc).to be_a(Proc)
24
+ end
25
+
26
+ it "caches compiled expressions" do
27
+ ast = Moxml::XPath::Parser.parse("/root")
28
+ proc1 = described_class.compile_with_cache(ast)
29
+ proc2 = described_class.compile_with_cache(ast)
30
+
31
+ expect(proc1).to equal(proc2)
32
+ end
33
+
34
+ it "uses different cache keys for different namespaces" do
35
+ ast = Moxml::XPath::Parser.parse("/root")
36
+ proc1 = described_class.compile_with_cache(ast,
37
+ namespaces: { "x" => "http://example.com" })
38
+ proc2 = described_class.compile_with_cache(ast,
39
+ namespaces: { "y" => "http://other.com" })
40
+
41
+ expect(proc1).not_to equal(proc2)
42
+ end
43
+ end
44
+
45
+ describe "Basic compilation" do
46
+ it "compiles and executes /root" do
47
+ ast = Moxml::XPath::Parser.parse("/root")
48
+ proc = described_class.compile_with_cache(ast)
49
+ result = proc.call(doc)
50
+
51
+ expect(result).to be_a(Moxml::NodeSet)
52
+ expect(result.size).to eq(1)
53
+ expect(result.first.name).to eq("root")
54
+ end
55
+
56
+ it "compiles and executes /root/child" do
57
+ ast = Moxml::XPath::Parser.parse("/root/child")
58
+ proc = described_class.compile_with_cache(ast)
59
+ result = proc.call(doc)
60
+
61
+ expect(result).to be_a(Moxml::NodeSet)
62
+ expect(result.size).to eq(2)
63
+ expect(result.map(&:name)).to eq(["child", "child"])
64
+ end
65
+
66
+ it "compiles and executes /root/other" do
67
+ ast = Moxml::XPath::Parser.parse("/root/other")
68
+ proc = described_class.compile_with_cache(ast)
69
+ result = proc.call(doc)
70
+
71
+ expect(result).to be_a(Moxml::NodeSet)
72
+ expect(result.size).to eq(1)
73
+ expect(result.first.name).to eq("other")
74
+ end
75
+ end
76
+
77
+ describe "Axis: child" do
78
+ it "selects direct children" do
79
+ ast = Moxml::XPath::Parser.parse("/root/child")
80
+ proc = described_class.compile_with_cache(ast)
81
+ result = proc.call(doc)
82
+
83
+ expect(result.size).to eq(2)
84
+ expect(result.map(&:name)).to all(eq("child"))
85
+ end
86
+
87
+ it "returns empty set when no children match" do
88
+ ast = Moxml::XPath::Parser.parse("/root/nonexistent")
89
+ proc = described_class.compile_with_cache(ast)
90
+ result = proc.call(doc)
91
+
92
+ expect(result).to be_empty
93
+ end
94
+ end
95
+
96
+ describe "Axis: self" do
97
+ it "selects the node itself" do
98
+ ast = Moxml::XPath::Parser.parse("/root/self::root")
99
+ proc = described_class.compile_with_cache(ast)
100
+ result = proc.call(doc)
101
+
102
+ expect(result.size).to eq(1)
103
+ expect(result.first.name).to eq("root")
104
+ end
105
+ end
106
+
107
+ describe "Axis: parent" do
108
+ it "selects parent node" do
109
+ ast = Moxml::XPath::Parser.parse("/root/child/parent::root")
110
+ proc = described_class.compile_with_cache(ast)
111
+ result = proc.call(doc)
112
+
113
+ expect(result.size).to eq(2) # Two child elements have same parent
114
+ expect(result.first.name).to eq("root")
115
+ end
116
+ end
117
+
118
+ describe "Axis: descendant-or-self (//)" do
119
+ let(:nested_xml) do
120
+ <<~XML
121
+ <root>
122
+ <book price="10">
123
+ <title>Programming Ruby</title>
124
+ <author>Matz</author>
125
+ </book>
126
+ <book price="20">
127
+ <title>Programming Python</title>
128
+ <author>Guido</author>
129
+ </book>
130
+ </root>
131
+ XML
132
+ end
133
+ let(:nested_doc) { context.parse(nested_xml) }
134
+
135
+ it "finds all descendants with //" do
136
+ ast = Moxml::XPath::Parser.parse("//title")
137
+ proc = described_class.compile_with_cache(ast)
138
+ result = proc.call(nested_doc)
139
+
140
+ expect(result).to be_a(Moxml::NodeSet)
141
+ expect(result.size).to eq(2)
142
+ expect(result.map(&:text)).to contain_exactly("Programming Ruby",
143
+ "Programming Python")
144
+ end
145
+
146
+ it "finds nested elements" do
147
+ ast = Moxml::XPath::Parser.parse("//author")
148
+ proc = described_class.compile_with_cache(ast)
149
+ result = proc.call(nested_doc)
150
+
151
+ expect(result.size).to eq(2)
152
+ expect(result.map(&:text)).to contain_exactly("Matz", "Guido")
153
+ end
154
+
155
+ it "works with wildcards" do
156
+ skip "HeadedOx limitation: Wildcard count differs due to Ox's DOM structure. See docs/HEADED_OX_LIMITATIONS.md"
157
+ ast = Moxml::XPath::Parser.parse("//*")
158
+ proc = described_class.compile_with_cache(ast)
159
+ result = proc.call(nested_doc)
160
+
161
+ # Should find root, 2 books, 2 titles, 2 authors = 7 elements
162
+ expect(result.size).to be >= 7
163
+ end
164
+ end
165
+
166
+ describe "Axis: attribute (@)" do
167
+ let(:attr_xml) do
168
+ <<~XML
169
+ <root>
170
+ <book price="10" isbn="123">
171
+ <title lang="en">Book 1</title>
172
+ </book>
173
+ <book price="20" isbn="456">
174
+ <title lang="fr">Book 2</title>
175
+ </book>
176
+ </root>
177
+ XML
178
+ end
179
+ let(:attr_doc) { context.parse(attr_xml) }
180
+
181
+ it "selects attributes with @" do
182
+ ast = Moxml::XPath::Parser.parse("/root/book/@price")
183
+ proc = described_class.compile_with_cache(ast)
184
+ result = proc.call(attr_doc)
185
+
186
+ expect(result).to be_a(Moxml::NodeSet)
187
+ expect(result.size).to eq(2)
188
+ expect(result.map(&:value)).to contain_exactly("10", "20")
189
+ end
190
+
191
+ it "works with wildcards" do
192
+ skip "HeadedOx limitation: Attribute wildcard (@*) not supported by XPath parser. See docs/HEADED_OX_LIMITATIONS.md"
193
+ ast = Moxml::XPath::Parser.parse("/root/book/@*")
194
+ proc = described_class.compile_with_cache(ast)
195
+ result = proc.call(attr_doc)
196
+
197
+ # Each book has 2 attributes (price, isbn) = 4 total
198
+ expect(result.size).to eq(4)
199
+ end
200
+
201
+ it "selects nested element attributes" do
202
+ ast = Moxml::XPath::Parser.parse("/root/book/title/@lang")
203
+ proc = described_class.compile_with_cache(ast)
204
+ result = proc.call(attr_doc)
205
+
206
+ expect(result.size).to eq(2)
207
+ expect(result.map(&:value)).to contain_exactly("en", "fr")
208
+ end
209
+
210
+ it "returns empty when no attributes match" do
211
+ ast = Moxml::XPath::Parser.parse("/root/book/@nonexistent")
212
+ proc = described_class.compile_with_cache(ast)
213
+ result = proc.call(attr_doc)
214
+
215
+ expect(result).to be_empty
216
+ end
217
+ end
218
+
219
+ describe "Axis: descendant" do
220
+ let(:desc_xml) do
221
+ <<~XML
222
+ <root>
223
+ <parent id="p1">
224
+ <child id="c1">
225
+ <grandchild id="g1">text1</grandchild>
226
+ </child>
227
+ <child id="c2">
228
+ <grandchild id="g2">text2</grandchild>
229
+ </child>
230
+ </parent>
231
+ </root>
232
+ XML
233
+ end
234
+ let(:desc_doc) { context.parse(desc_xml) }
235
+
236
+ it "finds all descendants without self" do
237
+ ast = Moxml::XPath::Parser.parse("/root/descendant::grandchild")
238
+ proc = described_class.compile_with_cache(ast)
239
+ result = proc.call(desc_doc)
240
+
241
+ expect(result).to be_a(Moxml::NodeSet)
242
+ expect(result.size).to eq(2)
243
+ expect(result.map do |n|
244
+ n.attribute("id")&.value
245
+ end).to contain_exactly("g1", "g2")
246
+ end
247
+
248
+ it "does not include the context node itself" do
249
+ ast = Moxml::XPath::Parser.parse("/root/parent/descendant::parent")
250
+ proc = described_class.compile_with_cache(ast)
251
+ result = proc.call(desc_doc)
252
+
253
+ # Should not find parent itself, only descendants named parent (none)
254
+ expect(result).to be_empty
255
+ end
256
+ end
257
+
258
+ describe "Node tests" do
259
+ it "matches element names" do
260
+ ast = Moxml::XPath::Parser.parse("/root/child")
261
+ proc = described_class.compile_with_cache(ast)
262
+ result = proc.call(doc)
263
+
264
+ expect(result.map(&:name)).to all(eq("child"))
265
+ end
266
+
267
+ it "handles wildcard" do
268
+ ast = Moxml::XPath::Parser.parse("/root/*")
269
+ proc = described_class.compile_with_cache(ast)
270
+ result = proc.call(doc)
271
+
272
+ expect(result.size).to eq(3) # 2 child + 1 other
273
+ end
274
+
275
+ it "matches case-insensitively" do
276
+ xml_mixed = "<ROOT><Child>text</Child></ROOT>"
277
+ doc_mixed = context.parse(xml_mixed)
278
+
279
+ ast = Moxml::XPath::Parser.parse("/root/child")
280
+ proc = described_class.compile_with_cache(ast)
281
+ result = proc.call(doc_mixed)
282
+
283
+ expect(result.size).to eq(1)
284
+ end
285
+ end
286
+
287
+ describe "Literals" do
288
+ it "compiles string literals" do
289
+ ast = Moxml::XPath::AST::Node.string("hello")
290
+ proc = described_class.compile_with_cache(ast)
291
+ result = proc.call(doc)
292
+
293
+ expect(result).to eq("hello")
294
+ end
295
+
296
+ it "compiles number literals" do
297
+ ast = Moxml::XPath::AST::Node.number(42)
298
+ proc = described_class.compile_with_cache(ast)
299
+ result = proc.call(doc)
300
+
301
+ expect(result).to eq(42.0)
302
+ end
303
+
304
+ it "compiles float literals" do
305
+ ast = Moxml::XPath::AST::Node.number(3.14)
306
+ proc = described_class.compile_with_cache(ast)
307
+ result = proc.call(doc)
308
+
309
+ expect(result).to eq(3.14)
310
+ end
311
+ end
312
+
313
+ describe "Special nodes" do
314
+ it "handles current node (.)" do
315
+ ast = Moxml::XPath::Parser.parse(".")
316
+ proc = described_class.compile_with_cache(ast)
317
+ result = proc.call(doc)
318
+
319
+ expect(result).to eq(doc)
320
+ end
321
+
322
+ it "handles parent node (..)" do
323
+ # Get a child first
324
+ ast = Moxml::XPath::Parser.parse("/root/child")
325
+ proc = described_class.compile_with_cache(ast)
326
+ children = proc.call(doc)
327
+
328
+ # Now get parent from child
329
+ parent_ast = Moxml::XPath::Parser.parse("..")
330
+ parent_proc = described_class.compile_with_cache(parent_ast)
331
+ result = parent_proc.call(children.first)
332
+
333
+ expect(result.name).to eq("root")
334
+ end
335
+ end
336
+
337
+ describe "Complex paths" do
338
+ it "handles multi-step paths" do
339
+ xml_nested = <<~XML
340
+ <root>
341
+ <level1>
342
+ <level2>
343
+ <target>found</target>
344
+ </level2>
345
+ </level1>
346
+ </root>
347
+ XML
348
+ doc_nested = context.parse(xml_nested)
349
+
350
+ ast = Moxml::XPath::Parser.parse("/root/level1/level2/target")
351
+ proc = described_class.compile_with_cache(ast)
352
+ result = proc.call(doc_nested)
353
+
354
+ expect(result.size).to eq(1)
355
+ expect(result.first.name).to eq("target")
356
+ end
357
+
358
+ it "handles paths that return no results" do
359
+ ast = Moxml::XPath::Parser.parse("/root/nonexistent/child")
360
+ proc = described_class.compile_with_cache(ast)
361
+ result = proc.call(doc)
362
+
363
+ expect(result).to be_empty
364
+ end
365
+ end
366
+
367
+ describe "Root node handling" do
368
+ it "selects document root with /" do
369
+ ast = Moxml::XPath::Parser.parse("/")
370
+ proc = described_class.compile_with_cache(ast)
371
+ result = proc.call(doc)
372
+
373
+ expect(result).to be_a(Moxml::NodeSet)
374
+ expect(result.size).to eq(1)
375
+ # Root should be the document or root element
376
+ end
377
+ end
378
+
379
+ describe "Error handling" do
380
+ it "handles malformed AST gracefully" do
381
+ # Create an AST with an unknown type
382
+ ast = Moxml::XPath::AST::Node.new(:unknown_type)
383
+
384
+ expect do
385
+ described_class.compile_with_cache(ast)
386
+ end.to raise_error(NoMethodError, /on_unknown_type/)
387
+ end
388
+ end
389
+
390
+ describe "Cache behavior" do
391
+ it "limits cache size" do
392
+ # Generate many different expressions
393
+ cache = Moxml::XPath::Cache.new(5)
394
+ compiler_class = Class.new(described_class) do
395
+ const_set(:CACHE, cache)
396
+ end
397
+
398
+ 10.times do |i|
399
+ ast = Moxml::XPath::Parser.parse("/root/child#{i}")
400
+ compiler_class.compile_with_cache(ast)
401
+ end
402
+
403
+ expect(cache.size).to be <= 5
404
+ end
405
+ end
406
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::Context do
6
+ let(:context) { described_class.new }
7
+
8
+ describe "#evaluate" do
9
+ it "evaluates simple Ruby code" do
10
+ result = context.evaluate("1 + 1")
11
+ expect(result).to eq(2)
12
+ end
13
+
14
+ it "evaluates string expressions" do
15
+ result = context.evaluate('"hello" + " world"')
16
+ expect(result).to eq("hello world")
17
+ end
18
+
19
+ it "evaluates boolean expressions" do
20
+ result = context.evaluate("true && false")
21
+ expect(result).to be(false)
22
+ end
23
+
24
+ it "evaluates arithmetic expressions" do
25
+ result = context.evaluate("10 * 5 + 3")
26
+ expect(result).to eq(53)
27
+ end
28
+
29
+ it "evaluates method calls" do
30
+ result = context.evaluate('"hello".upcase')
31
+ expect(result).to eq("HELLO")
32
+ end
33
+
34
+ it "evaluates array operations" do
35
+ result = context.evaluate("[1, 2, 3].map { |x| x * 2 }")
36
+ expect(result).to eq([2, 4, 6])
37
+ end
38
+
39
+ it "evaluates variable assignments and usage" do
40
+ code = <<~RUBY
41
+ x = 10
42
+ y = 20
43
+ x + y
44
+ RUBY
45
+ result = context.evaluate(code)
46
+ expect(result).to eq(30)
47
+ end
48
+
49
+ it "evaluates conditional statements" do
50
+ code = <<~RUBY
51
+ x = 15
52
+ if x > 10
53
+ "big"
54
+ else
55
+ "small"
56
+ end
57
+ RUBY
58
+ result = context.evaluate(code)
59
+ expect(result).to eq("big")
60
+ end
61
+
62
+ it "evaluates loops" do
63
+ code = <<~RUBY
64
+ sum = 0
65
+ [1, 2, 3, 4, 5].each do |n|
66
+ sum += n
67
+ end
68
+ sum
69
+ RUBY
70
+ result = context.evaluate(code)
71
+ expect(result).to eq(15)
72
+ end
73
+
74
+ it "returns the value of the last expression" do
75
+ code = <<~RUBY
76
+ x = 10
77
+ y = 20
78
+ x
79
+ y
80
+ RUBY
81
+ result = context.evaluate(code)
82
+ expect(result).to eq(20)
83
+ end
84
+
85
+ it "handles lambda expressions" do
86
+ code = "lambda { |x| x * 2 }.call(5)"
87
+ result = context.evaluate(code)
88
+ expect(result).to eq(10)
89
+ end
90
+
91
+ it "handles proc expressions" do
92
+ code = "proc { |x| x + 1 }.call(9)"
93
+ result = context.evaluate(code)
94
+ expect(result).to eq(10)
95
+ end
96
+
97
+ it "raises SyntaxError for invalid Ruby code" do
98
+ expect do
99
+ context.evaluate("def invalid syntax")
100
+ end.to raise_error(SyntaxError)
101
+ end
102
+
103
+ it "raises NameError for undefined variables" do
104
+ expect do
105
+ context.evaluate("undefined_variable")
106
+ end.to raise_error(NameError)
107
+ end
108
+
109
+ it "provides isolated binding between calls" do
110
+ context.evaluate("x = 100")
111
+ # Each evaluate call should have access to previously defined variables
112
+ # in the same context's binding
113
+ result = context.evaluate("x")
114
+ expect(result).to eq(100)
115
+ end
116
+
117
+ it "can define and call methods" do
118
+ code = <<~RUBY
119
+ def add(a, b)
120
+ a + b
121
+ end
122
+ add(3, 4)
123
+ RUBY
124
+ result = context.evaluate(code)
125
+ expect(result).to eq(7)
126
+ end
127
+
128
+ it "handles complex nested structures" do
129
+ code = <<~RUBY
130
+ data = { a: [1, 2, 3], b: [4, 5, 6] }
131
+ data[:a].map { |x| x * 2 } + data[:b].map { |x| x * 3 }
132
+ RUBY
133
+ result = context.evaluate(code)
134
+ expect(result).to eq([2, 4, 6, 12, 15, 18])
135
+ end
136
+
137
+ it "returns lambda/proc objects" do
138
+ result = context.evaluate("lambda { |x| x * 2 }")
139
+ expect(result).to be_a(Proc)
140
+ expect(result.call(5)).to eq(10)
141
+ end
142
+
143
+ it "handles begin/rescue/end blocks" do
144
+ code = <<~RUBY
145
+ begin
146
+ 1 / 0
147
+ rescue ZeroDivisionError
148
+ "error caught"
149
+ end
150
+ RUBY
151
+ result = context.evaluate(code)
152
+ expect(result).to eq("error caught")
153
+ end
154
+ end
155
+
156
+ describe "binding isolation" do
157
+ it "maintains separate bindings for different context instances" do
158
+ context1 = described_class.new
159
+ context2 = described_class.new
160
+
161
+ context1.evaluate("x = 100")
162
+
163
+ # context2 should not have access to context1's variables
164
+ expect { context2.evaluate("x") }.to raise_error(NameError)
165
+ end
166
+
167
+ it "maintains state within a single context" do
168
+ context.evaluate("counter = 0")
169
+ context.evaluate("counter += 1")
170
+ context.evaluate("counter += 1")
171
+ result = context.evaluate("counter")
172
+
173
+ expect(result).to eq(2)
174
+ end
175
+ end
176
+
177
+ describe "edge cases" do
178
+ it "handles empty string" do
179
+ result = context.evaluate("")
180
+ expect(result).to be_nil
181
+ end
182
+
183
+ it "handles nil literal" do
184
+ result = context.evaluate("nil")
185
+ expect(result).to be_nil
186
+ end
187
+
188
+ it "handles true literal" do
189
+ result = context.evaluate("true")
190
+ expect(result).to be(true)
191
+ end
192
+
193
+ it "handles false literal" do
194
+ result = context.evaluate("false")
195
+ expect(result).to be(false)
196
+ end
197
+
198
+ it "handles multiline strings" do
199
+ code = <<~RUBY
200
+ str = <<~TEXT
201
+ Hello
202
+ World
203
+ TEXT
204
+ str.strip
205
+ RUBY
206
+ result = context.evaluate(code)
207
+ expect(result).to eq("Hello\nWorld")
208
+ end
209
+ end
210
+ end