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.
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 +12 -4
  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 -3
  80. data/lib/moxml/adapter/customized_ox/namespace.rb +0 -2
  81. data/lib/moxml/adapter/customized_ox/text.rb +0 -2
  82. data/lib/moxml/adapter/customized_rexml/formatter.rb +11 -6
  83. data/lib/moxml/adapter/headed_ox.rb +161 -0
  84. data/lib/moxml/adapter/libxml.rb +1548 -0
  85. data/lib/moxml/adapter/nokogiri.rb +121 -9
  86. data/lib/moxml/adapter/oga.rb +123 -12
  87. data/lib/moxml/adapter/ox.rb +283 -27
  88. data/lib/moxml/adapter/rexml.rb +127 -20
  89. data/lib/moxml/adapter.rb +21 -4
  90. data/lib/moxml/attribute.rb +6 -0
  91. data/lib/moxml/builder.rb +40 -4
  92. data/lib/moxml/config.rb +8 -3
  93. data/lib/moxml/context.rb +39 -1
  94. data/lib/moxml/doctype.rb +13 -1
  95. data/lib/moxml/document.rb +39 -6
  96. data/lib/moxml/document_builder.rb +27 -5
  97. data/lib/moxml/element.rb +71 -2
  98. data/lib/moxml/error.rb +175 -6
  99. data/lib/moxml/node.rb +94 -3
  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 +1768 -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 -2
  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_spec.rb +36 -0
  164. data/spec/moxml/doctype_spec.rb +33 -0
  165. data/spec/moxml/document_builder_spec.rb +30 -0
  166. data/spec/moxml/document_spec.rb +105 -0
  167. data/spec/moxml/element_spec.rb +143 -0
  168. data/spec/moxml/error_spec.rb +266 -22
  169. data/spec/{moxml_spec.rb → moxml/moxml_spec.rb} +9 -9
  170. data/spec/moxml/namespace_spec.rb +32 -0
  171. data/spec/moxml/node_set_spec.rb +39 -0
  172. data/spec/moxml/node_spec.rb +37 -0
  173. data/spec/moxml/processing_instruction_spec.rb +34 -0
  174. data/spec/moxml/sax_spec.rb +1067 -0
  175. data/spec/moxml/text_spec.rb +31 -0
  176. data/spec/moxml/version_spec.rb +14 -0
  177. data/spec/moxml/xml_utils/.gitkeep +0 -0
  178. data/spec/moxml/xml_utils/encoder_spec.rb +27 -0
  179. data/spec/moxml/xml_utils_spec.rb +49 -0
  180. data/spec/moxml/xpath/ast/node_spec.rb +83 -0
  181. data/spec/moxml/xpath/axes_spec.rb +296 -0
  182. data/spec/moxml/xpath/cache_spec.rb +358 -0
  183. data/spec/moxml/xpath/compiler_spec.rb +406 -0
  184. data/spec/moxml/xpath/context_spec.rb +210 -0
  185. data/spec/moxml/xpath/conversion_spec.rb +365 -0
  186. data/spec/moxml/xpath/fixtures/sample.xml +25 -0
  187. data/spec/moxml/xpath/functions/boolean_functions_spec.rb +114 -0
  188. data/spec/moxml/xpath/functions/node_functions_spec.rb +145 -0
  189. data/spec/moxml/xpath/functions/numeric_functions_spec.rb +164 -0
  190. data/spec/moxml/xpath/functions/position_functions_spec.rb +93 -0
  191. data/spec/moxml/xpath/functions/special_functions_spec.rb +89 -0
  192. data/spec/moxml/xpath/functions/string_functions_spec.rb +381 -0
  193. data/spec/moxml/xpath/lexer_spec.rb +488 -0
  194. data/spec/moxml/xpath/parser_integration_spec.rb +210 -0
  195. data/spec/moxml/xpath/parser_spec.rb +364 -0
  196. data/spec/moxml/xpath/ruby/generator_spec.rb +421 -0
  197. data/spec/moxml/xpath/ruby/node_spec.rb +291 -0
  198. data/spec/moxml/xpath_capabilities_spec.rb +199 -0
  199. data/spec/moxml/xpath_spec.rb +77 -0
  200. data/spec/performance/README.md +83 -0
  201. data/spec/performance/benchmark_spec.rb +64 -0
  202. data/spec/{support/shared_examples/examples/memory.rb → performance/memory_usage_spec.rb} +3 -1
  203. data/spec/{support/shared_examples/examples/thread_safety.rb → performance/thread_safety_spec.rb} +3 -1
  204. data/spec/performance/xpath_benchmark_spec.rb +259 -0
  205. data/spec/spec_helper.rb +58 -1
  206. data/spec/support/xml_matchers.rb +1 -1
  207. metadata +176 -35
  208. data/lib/ox/node.rb +0 -9
  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
@@ -4,68 +4,312 @@
4
4
  RSpec.describe "Moxml errors" do
5
5
  describe Moxml::Error do
6
6
  it "is a StandardError" do
7
- expect(Moxml::Error.new).to be_a(StandardError)
7
+ expect(described_class.new).to be_a(StandardError)
8
8
  end
9
9
  end
10
10
 
11
11
  describe Moxml::ParseError do
12
12
  it "includes line and column information" do
13
- error = Moxml::ParseError.new("Invalid XML", line: 5, column: 10)
13
+ error = described_class.new("Invalid XML", line: 5, column: 10)
14
14
  expect(error.line).to eq(5)
15
15
  expect(error.column).to eq(10)
16
- expect(error.message).to eq("Invalid XML")
16
+ # The base message should be unchanged
17
+ expect(error.message).to include("Invalid XML")
17
18
  end
18
19
 
19
20
  it "works without line and column" do
20
- error = Moxml::ParseError.new("Invalid XML")
21
+ error = described_class.new("Invalid XML")
21
22
  expect(error.line).to be_nil
22
23
  expect(error.column).to be_nil
23
24
  end
24
- end
25
25
 
26
- describe Moxml::ValidationError do
27
- it "handles validation errors" do
28
- error = Moxml::ValidationError.new("Invalid document structure")
29
- expect(error.message).to eq("Invalid document structure")
26
+ it "includes source context" do
27
+ error = described_class.new("Invalid XML", source: "<invalid>")
28
+ expect(error.source).to eq("<invalid>")
29
+ end
30
+
31
+ it "provides helpful error message with context" do
32
+ error = described_class.new("Invalid XML", line: 5, column: 10)
33
+ message = error.to_s
34
+ expect(message).to include("Invalid XML")
35
+ expect(message).to include("Line: 5")
36
+ expect(message).to include("Column: 10")
37
+ expect(message).to include("Hint:")
30
38
  end
31
39
  end
32
40
 
33
41
  describe Moxml::XPathError do
34
- it "handles XPath errors" do
35
- error = Moxml::XPathError.new("Invalid XPath expression")
36
- expect(error.message).to eq("Invalid XPath expression")
42
+ it "includes expression and adapter information" do
43
+ error = described_class.new(
44
+ "Invalid XPath",
45
+ expression: "//invalid[",
46
+ adapter: "Nokogiri",
47
+ )
48
+ expect(error.expression).to eq("//invalid[")
49
+ expect(error.adapter).to eq("Nokogiri")
50
+ end
51
+
52
+ it "provides helpful error message" do
53
+ error = described_class.new(
54
+ "Invalid XPath",
55
+ expression: "//test",
56
+ adapter: "REXML",
57
+ )
58
+ message = error.to_s
59
+ expect(message).to include("Invalid XPath")
60
+ expect(message).to include("Expression: //test")
61
+ expect(message).to include("Adapter: REXML")
62
+ expect(message).to include("Hint:")
63
+ end
64
+ end
65
+
66
+ describe Moxml::ValidationError do
67
+ it "includes node, constraint, and value information" do
68
+ error = described_class.new(
69
+ "Invalid version",
70
+ constraint: "version",
71
+ value: "2.0",
72
+ )
73
+ expect(error.constraint).to eq("version")
74
+ expect(error.value).to eq("2.0")
75
+ end
76
+
77
+ it "provides helpful error message" do
78
+ error = described_class.new(
79
+ "Invalid encoding",
80
+ constraint: "encoding",
81
+ value: "INVALID",
82
+ )
83
+ message = error.to_s
84
+ expect(message).to include("Invalid encoding")
85
+ expect(message).to include("Constraint: encoding")
86
+ expect(message).to include("Value: \"INVALID\"")
87
+ expect(message).to include("Hint:")
37
88
  end
38
89
  end
39
90
 
40
91
  describe Moxml::NamespaceError do
41
- it "handles namespace errors" do
42
- error = Moxml::NamespaceError.new("Invalid namespace URI")
43
- expect(error.message).to eq("Invalid namespace URI")
92
+ it "includes prefix, uri, and element information" do
93
+ error = described_class.new(
94
+ "Invalid namespace",
95
+ prefix: "ns",
96
+ uri: "http://example.com",
97
+ )
98
+ expect(error.prefix).to eq("ns")
99
+ expect(error.uri).to eq("http://example.com")
100
+ expect(error.message).to eq("Invalid namespace")
101
+ end
102
+
103
+ it "provides access to error attributes" do
104
+ error = described_class.new(
105
+ "Invalid namespace",
106
+ prefix: "ns",
107
+ uri: "invalid-uri",
108
+ )
109
+ expect(error.message).to include("Invalid namespace")
110
+ expect(error.prefix).to eq("ns")
111
+ expect(error.uri).to eq("invalid-uri")
112
+ end
113
+ end
114
+
115
+ describe Moxml::AdapterError do
116
+ it "includes adapter name and operation" do
117
+ error = described_class.new(
118
+ "Failed to load adapter",
119
+ adapter: "nokogiri",
120
+ operation: "load",
121
+ )
122
+ expect(error.adapter_name).to eq("nokogiri")
123
+ expect(error.operation).to eq("load")
124
+ end
125
+
126
+ it "includes native error information" do
127
+ native_err = StandardError.new("Gem not found")
128
+ error = described_class.new(
129
+ "Failed to load adapter",
130
+ adapter: "nokogiri",
131
+ native_error: native_err,
132
+ )
133
+ expect(error.native_error).to eq(native_err)
134
+ end
135
+
136
+ it "provides helpful error message" do
137
+ native_err = LoadError.new("cannot load such file")
138
+ error = described_class.new(
139
+ "Failed to load adapter",
140
+ adapter: "oga",
141
+ operation: "require",
142
+ native_error: native_err,
143
+ )
144
+ message = error.to_s
145
+ expect(message).to include("Failed to load adapter")
146
+ expect(message).to include("Adapter: oga")
147
+ expect(message).to include("Operation: require")
148
+ expect(message).to include("Native Error: LoadError")
149
+ expect(message).to include("Hint:")
150
+ end
151
+ end
152
+
153
+ describe Moxml::SerializationError do
154
+ it "includes node, adapter, and format information" do
155
+ error = described_class.new(
156
+ "Failed to serialize",
157
+ adapter: "LibXML",
158
+ format: "xml",
159
+ )
160
+ expect(error.adapter).to eq("LibXML")
161
+ expect(error.format).to eq("xml")
162
+ end
163
+
164
+ it "provides helpful error message" do
165
+ error = described_class.new(
166
+ "Failed to serialize",
167
+ adapter: "Ox",
168
+ format: "xml",
169
+ )
170
+ message = error.to_s
171
+ expect(message).to include("Failed to serialize")
172
+ expect(message).to include("Adapter: Ox")
173
+ expect(message).to include("Format: xml")
174
+ expect(message).to include("Hint:")
175
+ end
176
+ end
177
+
178
+ describe Moxml::DocumentStructureError do
179
+ it "includes attempted operation and state" do
180
+ error = described_class.new(
181
+ "Invalid operation",
182
+ operation: "add_child",
183
+ state: "no_root",
184
+ )
185
+ expect(error.attempted_operation).to eq("add_child")
186
+ expect(error.current_state).to eq("no_root")
187
+ end
188
+
189
+ it "provides helpful error message" do
190
+ error = described_class.new(
191
+ "Invalid operation",
192
+ operation: "set_root",
193
+ state: "root_already_exists",
194
+ )
195
+ message = error.to_s
196
+ expect(message).to include("Invalid operation")
197
+ expect(message).to include("Operation: set_root")
198
+ expect(message).to include("Current State: root_already_exists")
199
+ expect(message).to include("Hint:")
200
+ end
201
+ end
202
+
203
+ describe Moxml::AttributeError do
204
+ it "includes attribute name, element, and value" do
205
+ error = described_class.new(
206
+ "Invalid attribute",
207
+ name: "id",
208
+ value: 123,
209
+ )
210
+ expect(error.attribute_name).to eq("id")
211
+ expect(error.value).to eq(123)
212
+ end
213
+
214
+ it "provides helpful error message" do
215
+ error = described_class.new(
216
+ "Invalid attribute name",
217
+ name: "123invalid",
218
+ value: "test",
219
+ )
220
+ message = error.to_s
221
+ expect(message).to include("Invalid attribute name")
222
+ expect(message).to include("Attribute: 123invalid")
223
+ expect(message).to include("Value: \"test\"")
224
+ expect(message).to include("Hint:")
225
+ end
226
+ end
227
+
228
+ describe Moxml::NotImplementedError do
229
+ it "includes feature and adapter information" do
230
+ error = described_class.new(
231
+ "Feature not supported",
232
+ feature: "xpath",
233
+ adapter: "CustomAdapter",
234
+ )
235
+ expect(error.feature).to eq("xpath")
236
+ expect(error.adapter).to eq("CustomAdapter")
237
+ end
238
+
239
+ it "provides helpful error message" do
240
+ error = described_class.new(
241
+ "Feature not supported",
242
+ feature: "namespaces",
243
+ adapter: "MinimalAdapter",
244
+ )
245
+ message = error.to_s
246
+ expect(message).to include("Feature not supported")
247
+ expect(message).to include("Feature: namespaces")
248
+ expect(message).to include("Adapter: MinimalAdapter")
249
+ expect(message).to include("Hint:")
250
+ end
251
+
252
+ it "has a default message" do
253
+ error = described_class.new
254
+ expect(error.message).to include("Feature not implemented")
44
255
  end
45
256
  end
46
257
 
47
258
  describe "error handling in context" do
48
259
  let(:context) { Moxml.new }
49
260
 
50
- it "raises ParseError for invalid XML" do
261
+ it "raises ParseError for invalid XML with enhanced context" do
51
262
  expect do
52
- context.parse("<invalid>")
53
- end.to raise_error(Moxml::ParseError)
263
+ context.parse("<invalid>", strict: true)
264
+ end.to raise_error(Moxml::ParseError) do |error|
265
+ expect(error.to_s).to include("Hint:")
266
+ end
54
267
  end
55
268
 
56
- it "raises XPathError for invalid XPath" do
269
+ it "raises XPathError for invalid XPath with enhanced context" do
57
270
  doc = context.parse("<root/>")
58
271
  expect do
59
272
  doc.xpath("///")
60
- end.to raise_error(Moxml::XPathError)
273
+ end.to raise_error(Moxml::XPathError) do |error|
274
+ expect(error.expression).to eq("///")
275
+ expect(error.to_s).to include("Hint:")
276
+ end
61
277
  end
62
278
 
63
- it "raises NamespaceError for invalid namespace" do
279
+ it "raises NamespaceError for invalid namespace with enhanced context" do
64
280
  doc = context.parse("<root/>")
65
281
 
66
282
  expect do
67
283
  doc.root.add_namespace("xml", "http//invalid.com")
68
- end.to raise_error(Moxml::NamespaceError, "Invalid URI: http//invalid.com")
284
+ end.to raise_error(Moxml::NamespaceError) do |error|
285
+ expect(error.uri).to eq("http//invalid.com")
286
+ expect(error.prefix).to eq("xml")
287
+ expect(error.message).to include("Invalid URI")
288
+ end
289
+ end
290
+
291
+ it "raises ValidationError for invalid XML constructs" do
292
+ expect do
293
+ context.parse('<?xml version="2.0"?><root/>', strict: true)
294
+ end.to raise_error(Moxml::ParseError)
295
+ end
296
+
297
+ it "raises AdapterError for invalid adapter configuration" do
298
+ expect do
299
+ Moxml::Config.new.adapter = :invalid_adapter
300
+ end.to raise_error(Moxml::AdapterError) do |error|
301
+ expect(error.adapter_name).to eq(:invalid_adapter)
302
+ expect(error.to_s).to include("Hint:")
303
+ end
304
+ end
305
+
306
+ it "raises DocumentStructureError for invalid node operations" do
307
+ doc = context.parse("<root/>")
308
+ expect do
309
+ doc.root.add_child(Object.new)
310
+ end.to raise_error(Moxml::DocumentStructureError) do |error|
311
+ expect(error.to_s).to include("Hint:")
312
+ end
69
313
  end
70
314
  end
71
315
  end
@@ -8,42 +8,42 @@ RSpec.describe Moxml do
8
8
 
9
9
  describe ".new" do
10
10
  it "creates a new context" do
11
- expect(Moxml.new).to be_a(Moxml::Context)
11
+ expect(described_class.new).to be_a(Moxml::Context)
12
12
  end
13
13
 
14
14
  it "accepts adapter specification" do
15
- context = Moxml.new(:nokogiri)
15
+ context = described_class.new(:nokogiri)
16
16
  expect(context.config.adapter_name).to eq(:nokogiri)
17
17
  end
18
18
 
19
19
  it "raises error for invalid adapter" do
20
- expect { Moxml.new(:invalid) }.to raise_error(ArgumentError)
20
+ expect { described_class.new(:invalid) }.to raise_error(Moxml::AdapterError)
21
21
  end
22
22
  end
23
23
 
24
24
  describe ".configure" do
25
25
  around do |example|
26
26
  # preserve the original config because it may be changed in examples
27
- Moxml.with_config { example.run }
27
+ described_class.with_config { example.run }
28
28
  end
29
29
 
30
30
  it "sets default values without a block" do
31
- Moxml.configure
31
+ described_class.configure
32
32
 
33
- context = Moxml.new
33
+ context = described_class.new
34
34
  expect(context.config.adapter_name).to eq(:nokogiri)
35
35
  end
36
36
 
37
37
  it "uses configured options from the block" do
38
- Moxml.configure do |config|
38
+ described_class.configure do |config|
39
39
  config.default_adapter = :oga
40
40
  config.strict_parsing = false
41
41
  config.default_encoding = "US-ASCII"
42
42
  end
43
43
 
44
- context = Moxml.new
44
+ context = described_class.new
45
45
  expect(context.config.adapter_name).to eq(:oga)
46
- expect(context.config.strict_parsing).to eq(false)
46
+ expect(context.config.strict_parsing).to be(false)
47
47
  expect(context.config.default_encoding).to eq("US-ASCII")
48
48
  end
49
49
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Namespace do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) do
8
+ context.parse('<root xmlns:ns="http://example.com"><ns:child/></root>')
9
+ end
10
+
11
+ describe "#prefix" do
12
+ it "returns namespace prefix" do
13
+ ns = doc.root.namespace
14
+ expect(ns).to be_nil # root has no prefix
15
+ end
16
+ end
17
+
18
+ describe "#uri" do
19
+ it "returns namespace URI" do
20
+ child = doc.root.children.first
21
+ ns = child.namespace
22
+ expect(ns.uri).to eq("http://example.com") if ns
23
+ end
24
+ end
25
+
26
+ describe "namespace handling" do
27
+ it "handles namespaces on elements" do
28
+ elem = doc.root.children.first
29
+ expect(elem.name).to include("child")
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::NodeSet do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root><child/><child/><child/></root>") }
8
+
9
+ describe "#length" do
10
+ it "returns number of nodes" do
11
+ nodes = doc.root.children
12
+ expect(nodes.length).to eq(3)
13
+ end
14
+ end
15
+
16
+ describe "#[]" do
17
+ it "accesses nodes by index" do
18
+ nodes = doc.root.children
19
+ expect(nodes[0]).to be_a(Moxml::Element)
20
+ expect(nodes[0].name).to eq("child")
21
+ end
22
+ end
23
+
24
+ describe "#each" do
25
+ it "iterates over nodes" do
26
+ nodes = doc.root.children
27
+ count = 0
28
+ nodes.each { |_node| count += 1 }
29
+ expect(count).to eq(3)
30
+ end
31
+ end
32
+
33
+ describe "#empty?" do
34
+ it "returns true when empty" do
35
+ nodes = doc.create_element("empty").children
36
+ expect(nodes).to be_empty
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::Node do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) { context.parse("<root><child>text</child></root>") }
8
+ let(:node) { doc.root }
9
+
10
+ describe "#parent" do
11
+ it "returns parent node" do
12
+ child = node.children.first
13
+ expect(child.parent).to eq(node)
14
+ end
15
+ end
16
+
17
+ describe "#document" do
18
+ it "returns document" do
19
+ expect(node.document).to eq(doc)
20
+ end
21
+ end
22
+
23
+ describe "#to_xml" do
24
+ it "serializes node to XML" do
25
+ expect(node.to_xml).to include("<root>")
26
+ expect(node.to_xml).to include("<child>")
27
+ end
28
+ end
29
+
30
+ describe "#remove" do
31
+ it "removes node from parent" do
32
+ child = node.children.first
33
+ child.remove
34
+ expect(node.children).to be_empty
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe Moxml::ProcessingInstruction do
6
+ let(:context) { Moxml.new }
7
+ let(:doc) do
8
+ context.parse('<?xml version="1.0"?><?xml-stylesheet type="text/xsl" href="style.xsl"?><root/>')
9
+ end
10
+
11
+ describe "#target" do
12
+ it "returns PI target" do
13
+ pi = doc.children.find { |n| n.is_a?(described_class) }
14
+ expect(pi.target).to eq("xml-stylesheet")
15
+ end
16
+ end
17
+
18
+ describe "#content" do
19
+ it "returns PI content" do
20
+ pi = doc.children.find { |n| n.is_a?(described_class) }
21
+ expect(pi.content).to include("type=")
22
+ expect(pi.content).to include("href=")
23
+ end
24
+ end
25
+
26
+ describe "creation" do
27
+ it "creates a processing instruction" do
28
+ pi = doc.create_processing_instruction("target", "content")
29
+ expect(pi).to be_a(described_class)
30
+ expect(pi.target).to eq("target")
31
+ expect(pi.content).to eq("content")
32
+ end
33
+ end
34
+ end