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.
Files changed (212) 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 +238 -40
  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 +45 -0
  21. data/docs/_guides/modifying-xml.adoc +293 -0
  22. data/docs/_guides/parsing-xml.adoc +231 -0
  23. data/docs/_guides/sax-parsing.adoc +603 -0
  24. data/docs/_guides/working-with-documents.adoc +118 -0
  25. data/docs/_pages/adapter-compatibility.adoc +369 -0
  26. data/docs/_pages/adapters/headed-ox.adoc +237 -0
  27. data/docs/_pages/adapters/index.adoc +98 -0
  28. data/docs/_pages/adapters/libxml.adoc +286 -0
  29. data/docs/_pages/adapters/nokogiri.adoc +252 -0
  30. data/docs/_pages/adapters/oga.adoc +292 -0
  31. data/docs/_pages/adapters/ox.adoc +55 -0
  32. data/docs/_pages/adapters/rexml.adoc +293 -0
  33. data/docs/_pages/best-practices.adoc +430 -0
  34. data/docs/_pages/compatibility.adoc +468 -0
  35. data/docs/_pages/configuration.adoc +251 -0
  36. data/docs/_pages/error-handling.adoc +350 -0
  37. data/docs/_pages/headed-ox-limitations.adoc +558 -0
  38. data/docs/_pages/headed-ox.adoc +1025 -0
  39. data/docs/_pages/index.adoc +35 -0
  40. data/docs/_pages/installation.adoc +141 -0
  41. data/docs/_pages/node-api-reference.adoc +50 -0
  42. data/docs/_pages/performance.adoc +36 -0
  43. data/docs/_pages/quick-start.adoc +244 -0
  44. data/docs/_pages/thread-safety.adoc +29 -0
  45. data/docs/_references/document-api.adoc +408 -0
  46. data/docs/_references/index.adoc +48 -0
  47. data/docs/_tutorials/basic-usage.adoc +268 -0
  48. data/docs/_tutorials/builder-pattern.adoc +343 -0
  49. data/docs/_tutorials/index.adoc +33 -0
  50. data/docs/_tutorials/namespace-handling.adoc +325 -0
  51. data/docs/_tutorials/xpath-queries.adoc +359 -0
  52. data/docs/index.adoc +122 -0
  53. data/examples/README.md +124 -0
  54. data/examples/api_client/README.md +424 -0
  55. data/examples/api_client/api_client.rb +394 -0
  56. data/examples/api_client/example_response.xml +48 -0
  57. data/examples/headed_ox_example/README.md +90 -0
  58. data/examples/headed_ox_example/headed_ox_demo.rb +71 -0
  59. data/examples/rss_parser/README.md +194 -0
  60. data/examples/rss_parser/example_feed.xml +93 -0
  61. data/examples/rss_parser/rss_parser.rb +189 -0
  62. data/examples/sax_parsing/README.md +50 -0
  63. data/examples/sax_parsing/data_extractor.rb +75 -0
  64. data/examples/sax_parsing/example.xml +21 -0
  65. data/examples/sax_parsing/large_file.rb +78 -0
  66. data/examples/sax_parsing/simple_parser.rb +55 -0
  67. data/examples/web_scraper/README.md +352 -0
  68. data/examples/web_scraper/example_page.html +201 -0
  69. data/examples/web_scraper/web_scraper.rb +312 -0
  70. data/lib/moxml/adapter/base.rb +107 -28
  71. data/lib/moxml/adapter/customized_libxml/cdata.rb +28 -0
  72. data/lib/moxml/adapter/customized_libxml/comment.rb +24 -0
  73. data/lib/moxml/adapter/customized_libxml/declaration.rb +85 -0
  74. data/lib/moxml/adapter/customized_libxml/element.rb +39 -0
  75. data/lib/moxml/adapter/customized_libxml/node.rb +44 -0
  76. data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +31 -0
  77. data/lib/moxml/adapter/customized_libxml/text.rb +27 -0
  78. data/lib/moxml/adapter/customized_oga/xml_generator.rb +1 -1
  79. data/lib/moxml/adapter/customized_ox/attribute.rb +28 -1
  80. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
  81. data/lib/moxml/adapter/headed_ox.rb +161 -0
  82. data/lib/moxml/adapter/libxml.rb +1548 -0
  83. data/lib/moxml/adapter/nokogiri.rb +121 -9
  84. data/lib/moxml/adapter/oga.rb +123 -12
  85. data/lib/moxml/adapter/ox.rb +282 -26
  86. data/lib/moxml/adapter/rexml.rb +127 -20
  87. data/lib/moxml/adapter.rb +21 -4
  88. data/lib/moxml/attribute.rb +6 -0
  89. data/lib/moxml/builder.rb +40 -4
  90. data/lib/moxml/config.rb +8 -3
  91. data/lib/moxml/context.rb +39 -1
  92. data/lib/moxml/doctype.rb +13 -1
  93. data/lib/moxml/document.rb +39 -6
  94. data/lib/moxml/document_builder.rb +27 -5
  95. data/lib/moxml/element.rb +71 -2
  96. data/lib/moxml/error.rb +175 -6
  97. data/lib/moxml/node.rb +94 -3
  98. data/lib/moxml/node_set.rb +34 -0
  99. data/lib/moxml/sax/block_handler.rb +194 -0
  100. data/lib/moxml/sax/element_handler.rb +124 -0
  101. data/lib/moxml/sax/handler.rb +113 -0
  102. data/lib/moxml/sax.rb +31 -0
  103. data/lib/moxml/version.rb +1 -1
  104. data/lib/moxml/xml_utils/encoder.rb +4 -4
  105. data/lib/moxml/xml_utils.rb +7 -4
  106. data/lib/moxml/xpath/ast/node.rb +159 -0
  107. data/lib/moxml/xpath/cache.rb +91 -0
  108. data/lib/moxml/xpath/compiler.rb +1768 -0
  109. data/lib/moxml/xpath/context.rb +26 -0
  110. data/lib/moxml/xpath/conversion.rb +124 -0
  111. data/lib/moxml/xpath/engine.rb +52 -0
  112. data/lib/moxml/xpath/errors.rb +101 -0
  113. data/lib/moxml/xpath/lexer.rb +304 -0
  114. data/lib/moxml/xpath/parser.rb +485 -0
  115. data/lib/moxml/xpath/ruby/generator.rb +269 -0
  116. data/lib/moxml/xpath/ruby/node.rb +193 -0
  117. data/lib/moxml/xpath.rb +37 -0
  118. data/lib/moxml.rb +5 -2
  119. data/moxml.gemspec +3 -1
  120. data/old-specs/moxml/adapter/customized_libxml/.gitkeep +6 -0
  121. data/spec/consistency/README.md +77 -0
  122. data/spec/{moxml/examples/adapter_spec.rb → consistency/adapter_parity_spec.rb} +4 -4
  123. data/spec/examples/README.md +75 -0
  124. data/spec/{support/shared_examples/examples/attribute.rb → examples/attribute_examples_spec.rb} +1 -1
  125. data/spec/{support/shared_examples/examples/basic_usage.rb → examples/basic_usage_spec.rb} +2 -2
  126. data/spec/{support/shared_examples/examples/namespace.rb → examples/namespace_examples_spec.rb} +3 -3
  127. data/spec/{support/shared_examples/examples/readme_examples.rb → examples/readme_examples_spec.rb} +6 -4
  128. data/spec/{support/shared_examples/examples/xpath.rb → examples/xpath_examples_spec.rb} +10 -6
  129. data/spec/integration/README.md +71 -0
  130. data/spec/{moxml/all_with_adapters_spec.rb → integration/all_adapters_spec.rb} +3 -2
  131. data/spec/integration/headed_ox_integration_spec.rb +326 -0
  132. data/spec/{support → integration}/shared_examples/edge_cases.rb +37 -10
  133. data/spec/integration/shared_examples/high_level/.gitkeep +0 -0
  134. data/spec/{support/shared_examples/context.rb → integration/shared_examples/high_level/context_behavior.rb} +2 -1
  135. data/spec/{support/shared_examples/integration.rb → integration/shared_examples/integration_workflows.rb} +23 -6
  136. data/spec/integration/shared_examples/node_wrappers/.gitkeep +0 -0
  137. data/spec/{support/shared_examples/cdata.rb → integration/shared_examples/node_wrappers/cdata_behavior.rb} +6 -1
  138. data/spec/{support/shared_examples/comment.rb → integration/shared_examples/node_wrappers/comment_behavior.rb} +2 -1
  139. data/spec/{support/shared_examples/declaration.rb → integration/shared_examples/node_wrappers/declaration_behavior.rb} +5 -2
  140. data/spec/{support/shared_examples/doctype.rb → integration/shared_examples/node_wrappers/doctype_behavior.rb} +2 -2
  141. data/spec/{support/shared_examples/document.rb → integration/shared_examples/node_wrappers/document_behavior.rb} +1 -1
  142. data/spec/{support/shared_examples/node.rb → integration/shared_examples/node_wrappers/node_behavior.rb} +9 -2
  143. data/spec/{support/shared_examples/node_set.rb → integration/shared_examples/node_wrappers/node_set_behavior.rb} +1 -18
  144. data/spec/{support/shared_examples/processing_instruction.rb → integration/shared_examples/node_wrappers/processing_instruction_behavior.rb} +6 -2
  145. data/spec/moxml/README.md +41 -0
  146. data/spec/moxml/adapter/.gitkeep +0 -0
  147. data/spec/moxml/adapter/README.md +61 -0
  148. data/spec/moxml/adapter/base_spec.rb +27 -0
  149. data/spec/moxml/adapter/headed_ox_spec.rb +311 -0
  150. data/spec/moxml/adapter/libxml_spec.rb +14 -0
  151. data/spec/moxml/adapter/ox_spec.rb +9 -8
  152. data/spec/moxml/adapter/shared_examples/.gitkeep +0 -0
  153. data/spec/{support/shared_examples/xml_adapter.rb → moxml/adapter/shared_examples/adapter_contract.rb} +39 -12
  154. data/spec/moxml/adapter_spec.rb +16 -0
  155. data/spec/moxml/attribute_spec.rb +30 -0
  156. data/spec/moxml/builder_spec.rb +33 -0
  157. data/spec/moxml/cdata_spec.rb +31 -0
  158. data/spec/moxml/comment_spec.rb +31 -0
  159. data/spec/moxml/config_spec.rb +3 -3
  160. data/spec/moxml/context_spec.rb +28 -0
  161. data/spec/moxml/declaration_spec.rb +36 -0
  162. data/spec/moxml/doctype_spec.rb +33 -0
  163. data/spec/moxml/document_builder_spec.rb +30 -0
  164. data/spec/moxml/document_spec.rb +105 -0
  165. data/spec/moxml/element_spec.rb +143 -0
  166. data/spec/moxml/error_spec.rb +266 -22
  167. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  168. data/spec/moxml/namespace_spec.rb +32 -0
  169. data/spec/moxml/node_set_spec.rb +39 -0
  170. data/spec/moxml/node_spec.rb +37 -0
  171. data/spec/moxml/processing_instruction_spec.rb +34 -0
  172. data/spec/moxml/sax_spec.rb +1067 -0
  173. data/spec/moxml/text_spec.rb +31 -0
  174. data/spec/moxml/version_spec.rb +14 -0
  175. data/spec/moxml/xml_utils/.gitkeep +0 -0
  176. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  177. data/spec/moxml/xml_utils_spec.rb +49 -0
  178. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  179. data/spec/moxml/xpath/axes_spec.rb +296 -0
  180. data/spec/moxml/xpath/cache_spec.rb +358 -0
  181. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  182. data/spec/moxml/xpath/context_spec.rb +210 -0
  183. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  184. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  185. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  186. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  187. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  188. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  189. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  190. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  191. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  192. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  193. data/spec/moxml/xpath/parser_spec.rb +364 -0
  194. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  195. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  196. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  197. data/spec/moxml/xpath_spec.rb +77 -0
  198. data/spec/performance/README.md +83 -0
  199. data/spec/performance/benchmark_spec.rb +64 -0
  200. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
  201. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  202. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  203. data/spec/spec_helper.rb +58 -1
  204. data/spec/support/xml_matchers.rb +1 -1
  205. metadata +176 -34
  206. data/spec/support/shared_examples/examples/benchmark_spec.rb +0 -51
  207. /data/spec/{support/shared_examples/builder.rb → integration/shared_examples/high_level/builder_behavior.rb} +0 -0
  208. /data/spec/{support/shared_examples/document_builder.rb → integration/shared_examples/high_level/document_builder_behavior.rb} +0 -0
  209. /data/spec/{support/shared_examples/attribute.rb → integration/shared_examples/node_wrappers/attribute_behavior.rb} +0 -0
  210. /data/spec/{support/shared_examples/element.rb → integration/shared_examples/node_wrappers/element_behavior.rb} +0 -0
  211. /data/spec/{support/shared_examples/namespace.rb → integration/shared_examples/node_wrappers/namespace_behavior.rb} +0 -0
  212. /data/spec/{support/shared_examples/text.rb → integration/shared_examples/node_wrappers/text_behavior.rb} +0 -0
@@ -0,0 +1,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::Ruby::Generator do
6
+ let(:generator) { described_class.new }
7
+ let(:node_class) { Moxml::XPath::Ruby::Node }
8
+
9
+ describe "#process" do
10
+ it "dispatches to the appropriate on_* method based on node type" do
11
+ node = node_class.new(:lit, ["10"])
12
+ expect(generator.process(node)).to eq("10")
13
+ end
14
+ end
15
+
16
+ describe "#on_followed_by" do
17
+ it "joins multiple statements with blank lines" do
18
+ stmt1 = node_class.new(:lit, ["a"])
19
+ stmt2 = node_class.new(:lit, ["b"])
20
+ stmt3 = node_class.new(:lit, ["c"])
21
+ node = node_class.new(:followed_by, [stmt1, stmt2, stmt3])
22
+
23
+ result = generator.process(node)
24
+ expect(result).to eq("a\n\nb\n\nc")
25
+ end
26
+
27
+ it "processes each child node" do
28
+ assign1 = node_class.new(:assign, [
29
+ node_class.new(:lit, ["x"]),
30
+ node_class.new(:lit, ["10"]),
31
+ ])
32
+ assign2 = node_class.new(:assign, [
33
+ node_class.new(:lit, ["y"]),
34
+ node_class.new(:lit, ["20"]),
35
+ ])
36
+ node = node_class.new(:followed_by, [assign1, assign2])
37
+
38
+ result = generator.process(node)
39
+ expect(result).to eq("x = 10\n\ny = 20")
40
+ end
41
+ end
42
+
43
+ describe "#on_assign" do
44
+ it "generates an assignment statement" do
45
+ var = node_class.new(:lit, ["x"])
46
+ val = node_class.new(:lit, ["10"])
47
+ node = node_class.new(:assign, [var, val])
48
+
49
+ result = generator.process(node)
50
+ expect(result).to eq("x = 10")
51
+ end
52
+
53
+ it "handles complex expressions" do
54
+ var = node_class.new(:lit, ["result"])
55
+ val = node_class.new(:send, [
56
+ node_class.new(:lit, ["a"]),
57
+ "+",
58
+ node_class.new(:lit, ["b"]),
59
+ ])
60
+ node = node_class.new(:assign, [var, val])
61
+
62
+ result = generator.process(node)
63
+ expect(result).to eq("result = a.+(b)")
64
+ end
65
+ end
66
+
67
+ describe "#on_massign" do
68
+ it "generates a multiple assignment statement" do
69
+ vars = [
70
+ node_class.new(:lit, ["x"]),
71
+ node_class.new(:lit, ["y"]),
72
+ ]
73
+ val = node_class.new(:lit, ["[1, 2]"])
74
+ node = node_class.new(:massign, [vars, val])
75
+
76
+ result = generator.process(node)
77
+ expect(result).to eq("x, y = [1, 2]")
78
+ end
79
+
80
+ it "handles three or more variables" do
81
+ vars = [
82
+ node_class.new(:lit, ["a"]),
83
+ node_class.new(:lit, ["b"]),
84
+ node_class.new(:lit, ["c"]),
85
+ ]
86
+ val = node_class.new(:lit, ["[1, 2, 3]"])
87
+ node = node_class.new(:massign, [vars, val])
88
+
89
+ result = generator.process(node)
90
+ expect(result).to eq("a, b, c = [1, 2, 3]")
91
+ end
92
+ end
93
+
94
+ describe "#on_begin" do
95
+ it "wraps code in a begin/end block" do
96
+ body = node_class.new(:lit, ["x + y"])
97
+ node = node_class.new(:begin, [body])
98
+
99
+ result = generator.process(node)
100
+ expect(result).to include("begin")
101
+ expect(result).to include("x + y")
102
+ expect(result).to include("end")
103
+ end
104
+ end
105
+
106
+ describe "#on_eq" do
107
+ it "generates an equality comparison" do
108
+ left = node_class.new(:lit, ["x"])
109
+ right = node_class.new(:lit, ["10"])
110
+ node = node_class.new(:eq, [left, right])
111
+
112
+ result = generator.process(node)
113
+ expect(result).to eq("x == 10")
114
+ end
115
+
116
+ it "handles complex expressions on both sides" do
117
+ left = node_class.new(:send, [
118
+ node_class.new(:lit, ["a"]),
119
+ "length",
120
+ ])
121
+ right = node_class.new(:lit, ["5"])
122
+ node = node_class.new(:eq, [left, right])
123
+
124
+ result = generator.process(node)
125
+ expect(result).to eq("a.length == 5")
126
+ end
127
+ end
128
+
129
+ describe "#on_and" do
130
+ it "generates a boolean and expression" do
131
+ left = node_class.new(:lit, ["x"])
132
+ right = node_class.new(:lit, ["y"])
133
+ node = node_class.new(:and, [left, right])
134
+
135
+ result = generator.process(node)
136
+ expect(result).to eq("x && y")
137
+ end
138
+ end
139
+
140
+ describe "#on_or" do
141
+ it "generates a boolean or expression with parentheses" do
142
+ left = node_class.new(:lit, ["x"])
143
+ right = node_class.new(:lit, ["y"])
144
+ node = node_class.new(:or, [left, right])
145
+
146
+ result = generator.process(node)
147
+ expect(result).to eq("(x || y)")
148
+ end
149
+ end
150
+
151
+ describe "#on_if" do
152
+ it "generates an if statement without else" do
153
+ condition = node_class.new(:lit, ["x > 10"])
154
+ body = node_class.new(:lit, ['puts "yes"'])
155
+ node = node_class.new(:if, [condition, body])
156
+
157
+ result = generator.process(node)
158
+ expect(result).to include("if x > 10")
159
+ expect(result).to include('puts "yes"')
160
+ expect(result).to include("end")
161
+ expect(result).not_to include("else")
162
+ end
163
+
164
+ it "generates an if/else statement when else_body is present" do
165
+ condition = node_class.new(:lit, ["x > 10"])
166
+ body = node_class.new(:lit, ['puts "yes"'])
167
+ else_body = node_class.new(:lit, ['puts "no"'])
168
+ node = node_class.new(:if, [condition, body, else_body])
169
+
170
+ result = generator.process(node)
171
+ expect(result).to include("if x > 10")
172
+ expect(result).to include('puts "yes"')
173
+ expect(result).to include("else")
174
+ expect(result).to include('puts "no"')
175
+ expect(result).to include("end")
176
+ end
177
+ end
178
+
179
+ describe "#on_while" do
180
+ it "generates a while loop" do
181
+ condition = node_class.new(:lit, ["x < 10"])
182
+ body = node_class.new(:lit, ["x += 1"])
183
+ node = node_class.new(:while, [condition, body])
184
+
185
+ result = generator.process(node)
186
+ expect(result).to include("while x < 10")
187
+ expect(result).to include("x += 1")
188
+ expect(result).to include("end")
189
+ end
190
+ end
191
+
192
+ describe "#on_send" do
193
+ it "generates a method call without receiver" do
194
+ node = node_class.new(:send,
195
+ [nil, "puts", node_class.new(:string, ["hello"])])
196
+
197
+ result = generator.process(node)
198
+ expect(result).to eq('puts("hello")')
199
+ end
200
+
201
+ it "generates a method call with receiver" do
202
+ receiver = node_class.new(:lit, ["str"])
203
+ node = node_class.new(:send, [receiver, "upcase"])
204
+
205
+ result = generator.process(node)
206
+ expect(result).to eq("str.upcase")
207
+ end
208
+
209
+ it "generates a method call with receiver and arguments" do
210
+ receiver = node_class.new(:lit, ["arr"])
211
+ arg = node_class.new(:lit, ["0"])
212
+ node = node_class.new(:send, [receiver, "at", arg])
213
+
214
+ result = generator.process(node)
215
+ expect(result).to eq("arr.at(0)")
216
+ end
217
+
218
+ it "handles multiple arguments" do
219
+ receiver = node_class.new(:lit, ["str"])
220
+ arg1 = node_class.new(:lit, ["0"])
221
+ arg2 = node_class.new(:lit, ["5"])
222
+ node = node_class.new(:send, [receiver, "slice", arg1, arg2])
223
+
224
+ result = generator.process(node)
225
+ expect(result).to eq("str.slice(0, 5)")
226
+ end
227
+
228
+ it "handles bracket notation for array access" do
229
+ receiver = node_class.new(:lit, ["arr"])
230
+ arg = node_class.new(:lit, ["0"])
231
+ node = node_class.new(:send, [receiver, "[]", arg])
232
+
233
+ result = generator.process(node)
234
+ expect(result).to eq("arr[0]")
235
+ end
236
+
237
+ it "handles bracket notation without arguments" do
238
+ receiver = node_class.new(:lit, ["arr"])
239
+ node = node_class.new(:send, [receiver, "[]"])
240
+
241
+ result = generator.process(node)
242
+ expect(result).to eq("arr[]")
243
+ end
244
+ end
245
+
246
+ describe "#on_block" do
247
+ it "generates a block with single argument" do
248
+ receiver = node_class.new(:send, [
249
+ node_class.new(:lit, ["items"]),
250
+ "each",
251
+ ])
252
+ arg = node_class.new(:lit, ["item"])
253
+ body = node_class.new(:send, [
254
+ nil,
255
+ "puts",
256
+ node_class.new(:lit, ["item"]),
257
+ ])
258
+ node = node_class.new(:block, [receiver, [arg], body])
259
+
260
+ result = generator.process(node)
261
+ expect(result).to include("items.each do |item|")
262
+ expect(result).to include("puts(item)")
263
+ expect(result).to include("end")
264
+ end
265
+
266
+ it "generates a block with multiple arguments" do
267
+ receiver = node_class.new(:send, [
268
+ node_class.new(:lit, ["hash"]),
269
+ "each",
270
+ ])
271
+ arg1 = node_class.new(:lit, ["key"])
272
+ arg2 = node_class.new(:lit, ["value"])
273
+ body = node_class.new(:lit, ["body"])
274
+ node = node_class.new(:block, [receiver, [arg1, arg2], body])
275
+
276
+ result = generator.process(node)
277
+ expect(result).to include("hash.each do |key, value|")
278
+ expect(result).to include("body")
279
+ end
280
+
281
+ it "handles blocks without body" do
282
+ receiver = node_class.new(:send, [
283
+ node_class.new(:lit, ["items"]),
284
+ "each",
285
+ ])
286
+ arg = node_class.new(:lit, ["item"])
287
+ node = node_class.new(:block, [receiver, [arg], nil])
288
+
289
+ result = generator.process(node)
290
+ expect(result).to include("items.each do |item|")
291
+ expect(result).to include("end")
292
+ end
293
+ end
294
+
295
+ describe "#on_range" do
296
+ it "generates a range expression" do
297
+ start = node_class.new(:lit, ["1"])
298
+ stop = node_class.new(:lit, ["10"])
299
+ node = node_class.new(:range, [start, stop])
300
+
301
+ result = generator.process(node)
302
+ expect(result).to eq("(1..10)")
303
+ end
304
+
305
+ it "handles complex expressions as range boundaries" do
306
+ start = node_class.new(:send, [
307
+ node_class.new(:lit, ["arr"]),
308
+ "first",
309
+ ])
310
+ stop = node_class.new(:send, [
311
+ node_class.new(:lit, ["arr"]),
312
+ "last",
313
+ ])
314
+ node = node_class.new(:range, [start, stop])
315
+
316
+ result = generator.process(node)
317
+ expect(result).to eq("(arr.first..arr.last)")
318
+ end
319
+ end
320
+
321
+ describe "#on_string" do
322
+ it "generates a string literal with proper escaping" do
323
+ node = node_class.new(:string, ["hello world"])
324
+
325
+ result = generator.process(node)
326
+ expect(result).to eq('"hello world"')
327
+ end
328
+
329
+ it "properly escapes special characters" do
330
+ node = node_class.new(:string, ['hello "world"'])
331
+
332
+ result = generator.process(node)
333
+ expect(result).to eq('"hello \"world\""')
334
+ end
335
+
336
+ it "handles newlines" do
337
+ node = node_class.new(:string, ["hello\nworld"])
338
+
339
+ result = generator.process(node)
340
+ expect(result).to eq('"hello\nworld"')
341
+ end
342
+ end
343
+
344
+ describe "#on_symbol" do
345
+ it "generates a symbol literal" do
346
+ node = node_class.new(:symbol, ["test"])
347
+
348
+ result = generator.process(node)
349
+ expect(result).to eq(":test")
350
+ end
351
+
352
+ it "handles symbols with special characters" do
353
+ node = node_class.new(:symbol, ["test_symbol"])
354
+
355
+ result = generator.process(node)
356
+ expect(result).to eq(":test_symbol")
357
+ end
358
+ end
359
+
360
+ describe "#on_lit" do
361
+ it "returns the literal value as-is" do
362
+ node = node_class.new(:lit, ["42"])
363
+
364
+ result = generator.process(node)
365
+ expect(result).to eq("42")
366
+ end
367
+
368
+ it "handles variable names" do
369
+ node = node_class.new(:lit, ["my_var"])
370
+
371
+ result = generator.process(node)
372
+ expect(result).to eq("my_var")
373
+ end
374
+
375
+ it "handles any string literal" do
376
+ node = node_class.new(:lit, ["some_expression"])
377
+
378
+ result = generator.process(node)
379
+ expect(result).to eq("some_expression")
380
+ end
381
+ end
382
+
383
+ describe "integration tests" do
384
+ it "generates valid Ruby code for complex expressions" do
385
+ # Create: x = 10; if x > 5; puts "big"; else; puts "small"; end
386
+ x_var = node_class.new(:lit, ["x"])
387
+ ten = node_class.new(:lit, ["10"])
388
+ assignment = x_var.assign(ten)
389
+
390
+ condition = node_class.new(:send,
391
+ [x_var, ">", node_class.new(:lit, ["5"])])
392
+ if_body = node_class.new(:send,
393
+ [nil, "puts", node_class.new(:string, ["big"])])
394
+ else_body = node_class.new(:send,
395
+ [nil, "puts",
396
+ node_class.new(:string, ["small"])])
397
+ if_stmt = node_class.new(:if, [condition, if_body, else_body])
398
+
399
+ program = assignment.followed_by(if_stmt)
400
+
401
+ code = generator.process(program)
402
+ expect(code).to be_a(String)
403
+
404
+ # Verify code can be parsed (basic syntax check)
405
+ expect { eval("lambda { #{code} }") }.not_to raise_error
406
+ end
407
+
408
+ it "generates code for nested blocks" do
409
+ # Create: [1,2,3].each { |x| puts x }
410
+ arr = node_class.new(:lit, ["[1,2,3]"])
411
+ each_call = node_class.new(:send, [arr, "each"])
412
+ arg = node_class.new(:lit, ["x"])
413
+ body = node_class.new(:send, [nil, "puts", node_class.new(:lit, ["x"])])
414
+ block = node_class.new(:block, [each_call, [arg], body])
415
+
416
+ code = generator.process(block)
417
+ expect(code).to include("[1,2,3].each do |x|")
418
+ expect(code).to include("puts(x)")
419
+ end
420
+ end
421
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::XPath::Ruby::Node do
6
+ describe "#initialize" do
7
+ it "creates a node with type and children" do
8
+ node = described_class.new(:lit, ["10"])
9
+ expect(node.type).to eq(:lit)
10
+ expect(node.to_a).to eq(["10"])
11
+ end
12
+
13
+ it "creates a node with empty children by default" do
14
+ node = described_class.new(:lit)
15
+ expect(node.type).to eq(:lit)
16
+ expect(node.to_a).to eq([])
17
+ end
18
+
19
+ it "converts type to symbol" do
20
+ node = described_class.new("lit", ["10"])
21
+ expect(node.type).to eq(:lit)
22
+ end
23
+ end
24
+
25
+ describe "#to_a and #to_ary" do
26
+ it "returns the children array" do
27
+ children = ["a", "b", "c"]
28
+ node = described_class.new(:test, children)
29
+ expect(node.to_a).to eq(children)
30
+ expect(node.to_ary).to eq(children)
31
+ end
32
+ end
33
+
34
+ describe "#to_array" do
35
+ it "returns a :send node calling to_a on the receiver" do
36
+ node = described_class.new(:lit, ["items"])
37
+ result = node.to_array
38
+
39
+ expect(result.type).to eq(:send)
40
+ expect(result.to_a).to eq([node, :to_a])
41
+ end
42
+ end
43
+
44
+ describe "#assign" do
45
+ it "creates an assignment node" do
46
+ var = described_class.new(:lit, ["x"])
47
+ val = described_class.new(:lit, ["10"])
48
+ result = var.assign(val)
49
+
50
+ expect(result.type).to eq(:assign)
51
+ expect(result.to_a).to eq([var, val])
52
+ end
53
+
54
+ it "wraps followed_by nodes in begin block" do
55
+ var = described_class.new(:lit, ["x"])
56
+ val = described_class.new(:followed_by, [
57
+ described_class.new(:lit, ["a"]),
58
+ described_class.new(:lit, ["b"]),
59
+ ])
60
+ result = var.assign(val)
61
+
62
+ expect(result.type).to eq(:assign)
63
+ assigned_val = result.to_a[1]
64
+ expect(assigned_val.type).to eq(:begin)
65
+ end
66
+ end
67
+
68
+ describe "#eq" do
69
+ it "creates an equality comparison node" do
70
+ left = described_class.new(:lit, ["10"])
71
+ right = described_class.new(:lit, ["20"])
72
+ result = left.eq(right)
73
+
74
+ expect(result.type).to eq(:eq)
75
+ expect(result.to_a).to eq([left, right])
76
+ end
77
+ end
78
+
79
+ describe "#and" do
80
+ it "creates a boolean and node" do
81
+ left = described_class.new(:lit, ["true"])
82
+ right = described_class.new(:lit, ["false"])
83
+ result = left.and(right)
84
+
85
+ expect(result.type).to eq(:and)
86
+ expect(result.to_a).to eq([left, right])
87
+ end
88
+ end
89
+
90
+ describe "#or" do
91
+ it "creates a boolean or node" do
92
+ left = described_class.new(:lit, ["true"])
93
+ right = described_class.new(:lit, ["false"])
94
+ result = left.or(right)
95
+
96
+ expect(result.type).to eq(:or)
97
+ expect(result.to_a).to eq([left, right])
98
+ end
99
+ end
100
+
101
+ describe "#not" do
102
+ it "creates a boolean not node using !" do
103
+ node = described_class.new(:lit, ["true"])
104
+ result = node.not
105
+
106
+ # The not method calls !self, which should create appropriate node
107
+ # Since Node inherits from BasicObject and undefines !,
108
+ # we need to verify the behavior
109
+ expect(result).to be_a(described_class)
110
+ end
111
+ end
112
+
113
+ describe "#is_a?" do
114
+ it "creates a :send node for is_a? method call" do
115
+ node = described_class.new(:lit, ["obj"])
116
+ result = node.is_a?(String)
117
+
118
+ expect(result.type).to eq(:send)
119
+ expect(result.to_a[0]).to eq(node)
120
+ expect(result.to_a[1]).to eq("is_a?")
121
+ expect(result.to_a[2].type).to eq(:lit)
122
+ expect(result.to_a[2].to_a[0]).to eq("String")
123
+ end
124
+ end
125
+
126
+ describe "#add_block" do
127
+ it "wraps node in a block with arguments and body" do
128
+ receiver = described_class.new(:lit, ["items"])
129
+ arg = described_class.new(:lit, ["item"])
130
+ body = described_class.new(:lit, ["body"])
131
+
132
+ result = receiver.add_block(arg) { body }
133
+
134
+ expect(result.type).to eq(:block)
135
+ expect(result.to_a[0]).to eq(receiver)
136
+ expect(result.to_a[1]).to eq([arg])
137
+ expect(result.to_a[2]).to eq(body)
138
+ end
139
+
140
+ it "accepts multiple arguments" do
141
+ receiver = described_class.new(:lit, ["hash"])
142
+ arg1 = described_class.new(:lit, ["key"])
143
+ arg2 = described_class.new(:lit, ["value"])
144
+ body = described_class.new(:lit, ["body"])
145
+
146
+ result = receiver.add_block(arg1, arg2) { body }
147
+
148
+ expect(result.type).to eq(:block)
149
+ expect(result.to_a[1]).to eq([arg1, arg2])
150
+ end
151
+ end
152
+
153
+ describe "#wrap" do
154
+ it "wraps the node in a begin node" do
155
+ node = described_class.new(:lit, ["10"])
156
+ result = node.wrap
157
+
158
+ expect(result.type).to eq(:begin)
159
+ expect(result.to_a).to eq([node])
160
+ end
161
+ end
162
+
163
+ describe "#if_true" do
164
+ it "creates an if statement with the node as condition" do
165
+ condition = described_class.new(:lit, ["x > 10"])
166
+ body = described_class.new(:lit, ['puts "yes"'])
167
+
168
+ result = condition.if_true { body }
169
+
170
+ expect(result.type).to eq(:if)
171
+ expect(result.to_a[0]).to eq(condition)
172
+ expect(result.to_a[1]).to eq(body)
173
+ end
174
+ end
175
+
176
+ describe "#if_false" do
177
+ it "creates an if !condition statement" do
178
+ condition = described_class.new(:lit, ["x > 10"])
179
+ body = described_class.new(:lit, ['puts "no"'])
180
+
181
+ result = condition.if_false { body }
182
+
183
+ expect(result.type).to eq(:if)
184
+ # Should have a not applied to condition
185
+ negated_condition = result.to_a[0]
186
+ expect(negated_condition).to be_a(described_class)
187
+ end
188
+ end
189
+
190
+ describe "#while_true" do
191
+ it "creates a while loop with the node as condition" do
192
+ condition = described_class.new(:lit, ["x < 10"])
193
+ body = described_class.new(:lit, ["x += 1"])
194
+
195
+ result = condition.while_true { body }
196
+
197
+ expect(result.type).to eq(:while)
198
+ expect(result.to_a[0]).to eq(condition)
199
+ expect(result.to_a[1]).to eq(body)
200
+ end
201
+ end
202
+
203
+ describe "#else" do
204
+ it "adds an else clause to an if node" do
205
+ condition = described_class.new(:lit, ["x > 10"])
206
+ if_body = described_class.new(:lit, ["a"])
207
+ else_body = described_class.new(:lit, ["b"])
208
+
209
+ if_node = condition.if_true { if_body }
210
+ result = if_node.else { else_body }
211
+
212
+ expect(result.type).to eq(:if)
213
+ expect(result.to_a[0]).to eq(condition)
214
+ expect(result.to_a[1]).to eq(if_body)
215
+ expect(result.to_a[2]).to eq(else_body)
216
+ end
217
+ end
218
+
219
+ describe "#followed_by" do
220
+ it "chains two nodes together" do
221
+ first = described_class.new(:lit, ["a"])
222
+ second = described_class.new(:lit, ["b"])
223
+
224
+ result = first.followed_by(second)
225
+
226
+ expect(result.type).to eq(:followed_by)
227
+ expect(result.to_a[0]).to eq(first)
228
+ expect(result.to_a[1]).to eq(second)
229
+ end
230
+
231
+ it "accepts a block to provide the second node" do
232
+ first = described_class.new(:lit, ["a"])
233
+ second = described_class.new(:lit, ["b"])
234
+
235
+ result = first.followed_by { second }
236
+
237
+ expect(result.type).to eq(:followed_by)
238
+ expect(result.to_a[0]).to eq(first)
239
+ expect(result.to_a[1]).to eq(second)
240
+ end
241
+ end
242
+
243
+ describe "#method_missing" do
244
+ it "creates a :send node for method calls without arguments" do
245
+ receiver = described_class.new(:lit, ["obj"])
246
+ result = receiver.foo
247
+
248
+ expect(result.type).to eq(:send)
249
+ expect(result.to_a[0]).to eq(receiver)
250
+ expect(result.to_a[1]).to eq("foo")
251
+ end
252
+
253
+ it "creates a :send node for method calls with arguments" do
254
+ receiver = described_class.new(:lit, ["obj"])
255
+ arg1 = described_class.new(:lit, ["a"])
256
+ arg2 = described_class.new(:lit, ["b"])
257
+
258
+ result = receiver.bar(arg1, arg2)
259
+
260
+ expect(result.type).to eq(:send)
261
+ expect(result.to_a[0]).to eq(receiver)
262
+ expect(result.to_a[1]).to eq("bar")
263
+ expect(result.to_a[2]).to eq(arg1)
264
+ expect(result.to_a[3]).to eq(arg2)
265
+ end
266
+
267
+ it "handles various method names" do
268
+ receiver = described_class.new(:lit, ["obj"])
269
+
270
+ # Test different method names
271
+ expect(receiver.length.to_a[1]).to eq("length")
272
+ expect(receiver.upcase.to_a[1]).to eq("upcase")
273
+ expect(receiver.custom_method.to_a[1]).to eq("custom_method")
274
+ end
275
+ end
276
+
277
+ describe "#inspect" do
278
+ it "returns a string representation of the node" do
279
+ node = described_class.new(:lit, ["10"])
280
+ expect(node.inspect).to eq('(lit "10")')
281
+ end
282
+
283
+ it "includes nested nodes in the representation" do
284
+ child1 = described_class.new(:lit, ["a"])
285
+ child2 = described_class.new(:lit, ["b"])
286
+ node = described_class.new(:eq, [child1, child2])
287
+
288
+ expect(node.inspect).to eq('(eq (lit "a") (lit "b"))')
289
+ end
290
+ end
291
+ end