diff --git a/.release-notes/36.md b/.release-notes/36.md
new file mode 100644
index 0000000..6bf9561
--- /dev/null
+++ b/.release-notes/36.md
@@ -0,0 +1,68 @@
+## Errors are now returned as data, not raised
+
+Parsing and XPath entry points no longer use Pony's partial-function `?` to signal failure. Instead they return a union of the success value and an `Xml2Error` value, so callers can inspect structured error information (domain, level, code, message, file, line, plus three context strings and two context integers).
+
+```pony
+match Xml2Parser.parseDoc(xml)
+| let doc: Xml2Doc =>
+ // use the document
+| let err: Xml2Error =>
+ env.err.print(err.string())
+end
+```
+
+### What changed
+
+- New `Xml2Parser.parseDoc(xml, options)` and `Xml2Parser.parseFile(auth, path, options)` entry points return `(Xml2Doc | Xml2Error)`.
+- New `xpathEvalNodes` / `xpathEvalString` / `xpathEvalF64` / `xpathEvalBool` convenience methods on `Xml2Doc` and `Xml2Node` return `(T | Xml2Error)` instead of raising.
+- `Xml2XPathResult` now includes `Xml2Error` as a variant; `xpathEval` populates it on evaluation failure. Empty nodesets now return an empty `Array[Xml2Node]` rather than `None`.
+- `Xml2Error` is now `class val` with `let` fields throughout; it is safe to share across actors.
+- `Xml2Error.domain` and `Xml2Error.level` are typed primitive unions (`Xml2ErrorDomain` and `Xml2ErrorLevel`) rather than free-form `String` values. Exhaustive `match` over these unions is supported.
+- `Xml2Error.string()` produces a human-readable rendering suitable for logging.
+- `Xml2Error.from_last_error()?` is the new (partial) constructor for reading libxml2's per-thread last-error directly; raises if there is no current error rather than silently fabricating one.
+
+### Breaking changes
+
+`Xml2Doc.parseDoc(...)?` and `Xml2Doc.parseFile(...)?` constructors have been removed. Migrate to `Xml2Parser.parseDoc(...)` / `Xml2Parser.parseFile(...)` and pattern-match the returned union:
+
+Before:
+
+```pony
+try
+ let doc = Xml2Doc.parseDoc(xml)?
+ // ...
+else
+ env.err.print("parse failed")
+end
+```
+
+After:
+
+```pony
+match Xml2Parser.parseDoc(xml)
+| let doc: Xml2Doc =>
+ // ...
+| let err: Xml2Error =>
+ env.err.print(err.string())
+end
+```
+
+The convenience XPath methods change shape similarly. Before:
+
+```pony
+try
+ let nodes = doc.xpathEvalNodes("//foo")?
+ // ...
+end
+```
+
+After:
+
+```pony
+match doc.xpathEvalNodes("//foo")
+| let nodes: Array[Xml2Node] => // ...
+| let err: Xml2Error => // ...
+end
+```
+
+Callers that previously relied on `Xml2Error.create()` reading thread-local last-error after a raise should instead receive the `Xml2Error` directly from the union return — this is reliable across Pony actor migration boundaries, which the previous mechanism was not.
diff --git a/README.md b/README.md
index 6ca18d6..356b703 100644
--- a/README.md
+++ b/README.md
@@ -29,15 +29,20 @@ If you're stuck, you can fall-back to the C API from the Pony API by using the C
For example, if you need to call an as-yet unimplemented function which takes an xmlDocPtr as an argument, you can extract it like this:
```pony
-let xmldoc: Xml2Doc = Xml2Doc.xmlParseFile(FileAuth(env.root), "somefile.xml")?
-let xmldocptr: NullablePointer[Xmldoc] = xmldoc.ptr'
+match Xml2Parser.parseFile(FileAuth(env.root), "somefile.xml")
+| let xmldoc: Xml2Doc =>
+ let xmldocptr: NullablePointer[XmlDoc] = xmldoc.ptr'
+ // ...
+| let err: Xml2Error =>
+ env.err.print("parse failed: " + err.string())
+end
```
If you need to go from the C API back to the Pony API, if implemented you can reverse it as follows:
```pony
-let s: NullablePointer[Xmlnode] = LibXML2.(some API call)
-let xmlnode: Xml2node = Xml2node.fromPTR(s)
+let s: NullablePointer[XmlNode] = LibXML2.(some API call)
+let xmlnode: Xml2Node = Xml2Node.fromPTR(s)
```
### How do I start?
diff --git a/libxml2/_test.pony b/libxml2/_test.pony
index fde083e..fee588f 100755
--- a/libxml2/_test.pony
+++ b/libxml2/_test.pony
@@ -23,6 +23,9 @@ actor \nodoc\ Main is TestList
test(TestXPathScalarResults)
test(TestXPathScalarResultsConvenience)
test(TestParseError)
+ test(TestXml2ErrorString)
+ test(TestXml2ErrorDomainFromI32)
+ test(TestXml2ErrorLevelFromI32)
test(TestGetProps)
test(TestModifyProps)
// Additional coverage tests
diff --git a/libxml2/_tests/basic_tests.pony b/libxml2/_tests/basic_tests.pony
index 5d75d86..79b6df4 100644
--- a/libxml2/_tests/basic_tests.pony
+++ b/libxml2/_tests/basic_tests.pony
@@ -14,7 +14,7 @@ class \nodoc\ iso TestModifyProps is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
h.assert_eq[String](root.nodeDump(0,0),
"""
@@ -59,7 +59,7 @@ class \nodoc\ iso TestGetProps is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
h.assert_eq[String]("root", root.name())
var children: Array[Xml2Node] = root.getChildren()
@@ -92,7 +92,7 @@ class \nodoc\ iso TestParseDocAndRoot is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
h.assert_eq[String]("root", root.name())
var children: Array[Xml2Node] = root.getChildren()
@@ -114,7 +114,7 @@ class \nodoc\ iso TestDocXPathSimpleNodeset is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let res = doc.xpathEval("//child")
match res
| let nodes: Array[Xml2Node] =>
@@ -140,8 +140,8 @@ class \nodoc\ iso TestDocXPathSimpleNodesetConvenience is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
- let nodes: Array[Xml2Node] = doc.xpathEvalNodes("//child")?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
+ let nodes: Array[Xml2Node] = doc.xpathEvalNodes("//child") as Array[Xml2Node]
h.assert_eq[USize](2, nodes.size())
h.assert_eq[String]("child", nodes(0)?.name())
h.assert_eq[String]("child", nodes(1)?.name())
@@ -166,7 +166,7 @@ class \nodoc\ iso TestNodeXPathRelative is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let sections = doc.xpathEval("//section")
match sections
@@ -206,12 +206,12 @@ class \nodoc\ iso TestNodeXPathRelativeConvenience is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
- let sec_nodes: Array[Xml2Node] = doc.xpathEvalNodes("//section")?
+ let sec_nodes: Array[Xml2Node] = doc.xpathEvalNodes("//section") as Array[Xml2Node]
h.assert_eq[USize](2, sec_nodes.size())
let first_sec = sec_nodes(0)?
- let items: Array[Xml2Node] = first_sec.xpathEvalNodes("./item")?
+ let items: Array[Xml2Node] = first_sec.xpathEvalNodes("./item") as Array[Xml2Node]
h.assert_eq[USize](2, items.size())
h.assert_eq[String]("one", items(0)?.getContent())
h.assert_eq[String]("two", items(1)?.getContent())
@@ -230,7 +230,7 @@ class \nodoc\ iso TestNodeAttributesAndContent is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let child_res = doc.xpathEval("//child")
match child_res
| let nodes: Array[Xml2Node] =>
@@ -260,7 +260,7 @@ class \nodoc\ iso TestXPathScalarResults is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// count() -> number
let count_res = doc.xpathEval("count(//child)")
@@ -304,18 +304,18 @@ class \nodoc\ iso TestXPathScalarResultsConvenience is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// count() -> number
- let n: F64 = doc.xpathEvalF64("count(//child)")?
+ let n: F64 = doc.xpathEvalF64("count(//child)") as F64
h.assert_true(n.usize() == 2)
// boolean() -> bool
- let b: Bool = doc.xpathEvalBool("boolean(//child[@id='c1'])")?
+ let b: Bool = doc.xpathEvalBool("boolean(//child[@id='c1'])") as Bool
h.assert_true(b)
// string() -> string
- let s: String val = doc.xpathEvalString("string(//child[@id='c2'])")?
+ let s: String val = doc.xpathEvalString("string(//child[@id='c2'])") as String val
h.assert_eq[String]("2", s)
else
h.fail("Exception in TestXPathScalarResults")
@@ -367,7 +367,7 @@ class \nodoc\ iso TestCreateAndAppendChildren is UnitTest
h.assert_true(xml.contains("World"))
// Verify XPath works on created documents
- let children = doc.xpathEvalNodes("//child")?
+ let children = doc.xpathEvalNodes("//child") as Array[Xml2Node]
h.assert_eq[USize](2, children.size())
else
h.fail("Failed to create and append children")
@@ -410,16 +410,16 @@ class \nodoc\ iso TestCreateAndXPath is UnitTest
root.appendChild(book2)?
// Test XPath queries on created document
- let books = doc.xpathEvalNodes("//book")?
+ let books = doc.xpathEvalNodes("//book") as Array[Xml2Node]
h.assert_eq[USize](2, books.size())
- let titles = doc.xpathEvalNodes("//title")?
+ let titles = doc.xpathEvalNodes("//title") as Array[Xml2Node]
h.assert_eq[USize](2, titles.size())
- let count = doc.xpathEvalF64("count(//book)")?
+ let count = doc.xpathEvalF64("count(//book)") as F64
h.assert_eq[USize](2, count.usize())
- let first_title = doc.xpathEvalString("string(//book[@id='bk101']/title)")?
+ let first_title = doc.xpathEvalString("string(//book[@id='bk101']/title)") as String val
h.assert_eq[String]("XML Developer's Guide", first_title)
else
h.fail("Failed XPath on created document")
@@ -441,7 +441,7 @@ class \nodoc\ iso TestCreateAndSaveFile is UnitTest
doc.saveToFile(auth, "test_created.xml")?
// Read back and verify
- let doc2 = Xml2Doc.parseFile(auth, "test_created.xml")?
+ let doc2 = Xml2Parser.parseFile(auth, "test_created.xml") as Xml2Doc
let root2 = doc2.getRootElement()?
h.assert_eq[String]("test", root2.name())
let children = root2.getChildren()
diff --git a/libxml2/_tests/coverage_tests.pony b/libxml2/_tests/coverage_tests.pony
index fbb3bba..92e0bec 100644
--- a/libxml2/_tests/coverage_tests.pony
+++ b/libxml2/_tests/coverage_tests.pony
@@ -18,7 +18,7 @@ class \nodoc\ iso TestNodeUtilityMethods is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
// Test getNodePath
@@ -69,7 +69,7 @@ class \nodoc\ iso TestGetLang is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
// Root should have lang="en"
@@ -106,7 +106,7 @@ class \nodoc\ iso TestEmptyNodeset is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// Query that matches nothing
let res = doc.xpathEval("//nonexistent")
@@ -122,7 +122,7 @@ class \nodoc\ iso TestEmptyNodeset is UnitTest
// Test convenience method throws on empty result
try
- let nodes = doc.xpathEvalNodes("//nonexistent")?
+ let nodes = doc.xpathEvalNodes("//nonexistent") as Array[Xml2Node]
h.assert_eq[USize](0, nodes.size())
end
else
@@ -143,7 +143,7 @@ class \nodoc\ iso TestNonExistentAttribute is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let children = root.getChildren()
let child = children(0)?
@@ -174,7 +174,7 @@ class \nodoc\ iso TestNoElementChildren is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let children = root.getChildren()
@@ -202,7 +202,7 @@ class \nodoc\ iso TestNodeDumpFormatting is UnitTest
hello
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
// No formatting (level=0, format=0)
@@ -237,27 +237,27 @@ class \nodoc\ iso TestXPathConvenienceWithNamespaces is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let ns: Array[(String val, String val)] =
[("ns", "http://example.com/ns")]
// Test xpathEvalNodes with namespaces
- let nodes = doc.xpathEvalNodes("//ns:item", ns)?
+ let nodes = doc.xpathEvalNodes("//ns:item", ns) as Array[Xml2Node]
h.assert_eq[USize](2, nodes.size())
// Test xpathEvalString with namespaces
- let str_val = doc.xpathEvalString("string(//ns:item[1])", ns)?
+ let str_val = doc.xpathEvalString("string(//ns:item[1])", ns) as String val
h.assert_eq[String]("value1", str_val)
// Test xpathEvalF64 with namespaces
- let count = doc.xpathEvalF64("count(//ns:item)", ns)?
+ let count = doc.xpathEvalF64("count(//ns:item)", ns) as F64
h.assert_true(count == 2.0)
// Test xpathEvalBool with namespaces
- let exists = doc.xpathEvalBool("boolean(//ns:item)", ns)?
+ let exists = doc.xpathEvalBool("boolean(//ns:item)", ns) as Bool
h.assert_true(exists)
- let not_exists = doc.xpathEvalBool("boolean(//ns:nonexistent)", ns)?
+ let not_exists = doc.xpathEvalBool("boolean(//ns:nonexistent)", ns) as Bool
h.assert_false(not_exists)
else
h.fail("Failed to parse XML or evaluate XPath")
@@ -280,7 +280,7 @@ class \nodoc\ iso TestNodeXPathConvenienceWithNamespaces is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let ns: Array[(String val, String val)] =
[("ns", "http://example.com/ns")]
@@ -289,20 +289,20 @@ class \nodoc\ iso TestNodeXPathConvenienceWithNamespaces is UnitTest
let container = containers(0)?
// Test node-level xpathEvalNodes with namespaces
- let items = container.xpathEvalNodes("./ns:item", ns)?
+ let items = container.xpathEvalNodes("./ns:item", ns) as Array[Xml2Node]
h.assert_eq[USize](2, items.size())
h.assert_eq[String]("one", items(0)?.getContent())
// Test node-level xpathEvalString with namespaces
- let str_val = container.xpathEvalString("string(./ns:item[2])", ns)?
+ let str_val = container.xpathEvalString("string(./ns:item[2])", ns) as String val
h.assert_eq[String]("two", str_val)
// Test node-level xpathEvalF64 with namespaces
- let count = container.xpathEvalF64("count(./ns:item)", ns)?
+ let count = container.xpathEvalF64("count(./ns:item)", ns) as F64
h.assert_true(count == 2.0)
// Test node-level xpathEvalBool with namespaces
- let exists = container.xpathEvalBool("boolean(./ns:item)", ns)?
+ let exists = container.xpathEvalBool("boolean(./ns:item)", ns) as Bool
h.assert_true(exists)
else
h.fail("Failed to parse XML or evaluate XPath")
@@ -322,7 +322,7 @@ class \nodoc\ iso TestXPathNoneResult is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// Invalid XPath should return None or error
let res = doc.xpathEval("")
@@ -353,7 +353,7 @@ class \nodoc\ iso TestMultipleAttributes is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let children = root.getChildren()
let item = children(0)?
@@ -418,10 +418,10 @@ class \nodoc\ iso TestDeepNesting is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// Test deep XPath query
- let nodes = doc.xpathEvalNodes("//level5")?
+ let nodes = doc.xpathEvalNodes("//level5") as Array[Xml2Node]
h.assert_eq[USize](1, nodes.size())
h.assert_eq[String]("deep content", nodes(0)?.getContent())
h.assert_eq[String](
@@ -463,25 +463,25 @@ class \nodoc\ iso TestXPathNumericOperations is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// Test sum function
- let total = doc.xpathEvalF64("sum(//item/@price)")?
+ let total = doc.xpathEvalF64("sum(//item/@price)") as F64
h.assert_true((total > 36.49) and (total < 36.51))
// Test count
- let count = doc.xpathEvalF64("count(//item)")?
+ let count = doc.xpathEvalF64("count(//item)") as F64
h.assert_true(count == 3.0)
// Test numeric comparison in boolean
- let has_expensive = doc.xpathEvalBool("boolean(//item[@price > 15])")?
+ let has_expensive = doc.xpathEvalBool("boolean(//item[@price > 15])") as Bool
h.assert_true(has_expensive)
- let has_cheap = doc.xpathEvalBool("boolean(//item[@price < 6])")?
+ let has_cheap = doc.xpathEvalBool("boolean(//item[@price < 6])") as Bool
h.assert_true(has_cheap)
let has_very_expensive =
- doc.xpathEvalBool("boolean(//item[@price > 100])")?
+ doc.xpathEvalBool("boolean(//item[@price > 100])") as Bool
h.assert_false(has_very_expensive)
else
h.fail("Failed to parse XML or evaluate XPath")
@@ -502,31 +502,31 @@ class \nodoc\ iso TestXPathStringFunctions is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// Test string-length
- let len = doc.xpathEvalF64("string-length(//item[1]/@name)")?
+ let len = doc.xpathEvalF64("string-length(//item[1]/@name)") as F64
h.assert_true(len == 11.0) // "Hello World" = 11 chars
// Test contains
- let has_hello = doc.xpathEvalBool("contains(//item[1]/@name, 'Hello')")?
+ let has_hello = doc.xpathEvalBool("contains(//item[1]/@name, 'Hello')") as Bool
h.assert_true(has_hello)
- let has_bye = doc.xpathEvalBool("contains(//item[1]/@name, 'Goodbye')")?
+ let has_bye = doc.xpathEvalBool("contains(//item[1]/@name, 'Goodbye')") as Bool
h.assert_false(has_bye)
// Test starts-with
let starts_hello =
- doc.xpathEvalBool("starts-with(//item[1]/@name, 'Hello')")?
+ doc.xpathEvalBool("starts-with(//item[1]/@name, 'Hello')") as Bool
h.assert_true(starts_hello)
// Test concat
let concat_result =
- doc.xpathEvalString("concat('prefix-', //item[1]/@name, '-suffix')")?
+ doc.xpathEvalString("concat('prefix-', //item[1]/@name, '-suffix')") as String val
h.assert_eq[String]("prefix-Hello World-suffix", concat_result)
// Test normalize-space
- let normalized = doc.xpathEvalString("normalize-space(//item[2]/@name)")?
+ let normalized = doc.xpathEvalString("normalize-space(//item[2]/@name)") as String val
h.assert_eq[String]("spaces", normalized)
else
h.fail("Failed to parse XML or evaluate XPath")
@@ -541,7 +541,7 @@ class \nodoc\ iso TestEmptyDocument is UnitTest
fun apply(h: TestHelper) =>
let xml = ""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
h.assert_eq[String]("root", root.name())
@@ -568,7 +568,7 @@ class \nodoc\ iso TestSpecialCharacters is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let children = root.getChildren()
let item = children(0)?
@@ -595,9 +595,9 @@ class \nodoc\ iso TestSerializeRoundTrip is UnitTest
fun apply(h: TestHelper) =>
let xml = "text"
try
- let doc1 = Xml2Doc.parseDoc(xml)?
+ let doc1 = Xml2Parser.parseDoc(xml) as Xml2Doc
let serialized = doc1.serialize(false)? // compact
- let doc2 = Xml2Doc.parseDoc(serialized)?
+ let doc2 = Xml2Parser.parseDoc(serialized) as Xml2Doc
// Verify structure is preserved
let root = doc2.getRootElement()?
@@ -620,7 +620,7 @@ class \nodoc\ iso TestSerializeFormatting is UnitTest
fun apply(h: TestHelper) =>
let xml = "text"
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// Compact should have no newlines in content
let compact = doc.serialize(false)?
@@ -647,11 +647,11 @@ class \nodoc\ iso TestSaveToFile is UnitTest
let auth = FileAuth(h.env.root)
// Save document
- let doc1 = Xml2Doc.parseDoc(xml)?
+ let doc1 = Xml2Parser.parseDoc(xml) as Xml2Doc
doc1.saveToFile(auth, temp_file)?
// Load it back
- let doc2 = Xml2Doc.parseFile(auth, temp_file)?
+ let doc2 = Xml2Parser.parseFile(auth, temp_file) as Xml2Doc
let root = doc2.getRootElement()?
h.assert_eq[String]("root", root.name())
@@ -675,7 +675,7 @@ class \nodoc\ iso TestSerializeEncoding is UnitTest
fun apply(h: TestHelper) =>
let xml = "Test content"
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// UTF-8 encoding (default)
let utf8 = doc.serialize(true, "UTF-8")?
@@ -698,7 +698,7 @@ class \nodoc\ iso TestSerializeModified is UnitTest
fun apply(h: TestHelper) =>
let xml = "text"
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let children = root.getChildren()
@@ -723,7 +723,7 @@ class \nodoc\ iso TestSerializeErrors is UnitTest
let xml = "test"
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let auth = FileAuth(h.env.root)
// Try to save to invalid path (should error)
@@ -816,7 +816,7 @@ class \nodoc\ iso TestAddChildConvenience is UnitTest
child2.setProp("id", "2")
// Verify structure using XPath
- let items = doc.xpathEvalNodes("//item")?
+ let items = doc.xpathEvalNodes("//item") as Array[Xml2Node]
h.assert_eq[USize](2, items.size())
// Verify attributes
@@ -895,18 +895,18 @@ class \nodoc\ iso TestComplexDocumentCreation is UnitTest
body.appendChild(doc.createComment("End of content")?)?
// Verify structure using XPath
- h.assert_eq[USize](1, doc.xpathEvalNodes("//html")?.size())
- h.assert_eq[USize](1, doc.xpathEvalNodes("//head")?.size())
- h.assert_eq[USize](1, doc.xpathEvalNodes("//body")?.size())
- h.assert_eq[USize](1, doc.xpathEvalNodes("//title")?.size())
- h.assert_eq[USize](1, doc.xpathEvalNodes("//h1")?.size())
- h.assert_eq[USize](1, doc.xpathEvalNodes("//p")?.size())
- h.assert_eq[USize](1, doc.xpathEvalNodes("//em")?.size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//html") as Array[Xml2Node]).size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//head") as Array[Xml2Node]).size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//body") as Array[Xml2Node]).size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//title") as Array[Xml2Node]).size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//h1") as Array[Xml2Node]).size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//p") as Array[Xml2Node]).size())
+ h.assert_eq[USize](1, (doc.xpathEvalNodes("//em") as Array[Xml2Node]).size())
// Verify content
- h.assert_eq[String]("Test Page", doc.xpathEvalString("string(//title)")?)
- h.assert_eq[String]("Welcome", doc.xpathEvalString("string(//h1)")?)
- h.assert_eq[String]("This is emphasized text.", doc.xpathEvalString("string(//p)")?)
+ h.assert_eq[String]("Test Page", doc.xpathEvalString("string(//title)") as String val)
+ h.assert_eq[String]("Welcome", doc.xpathEvalString("string(//h1)") as String val)
+ h.assert_eq[String]("This is emphasized text.", doc.xpathEvalString("string(//p)") as String val)
// Verify serialization
let xml = doc.serialize()?
@@ -949,11 +949,11 @@ class \nodoc\ iso TestXPathResultPostFreeAccess is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
// 1. Nodeset path: verify every node's name and content are readable
// after xpathEvalNodes returns (post-free correctness check).
- let nodes = doc.xpathEvalNodes("//item")?
+ let nodes = doc.xpathEvalNodes("//item") as Array[Xml2Node]
h.assert_eq[USize](4, nodes.size())
h.assert_eq[String]("item", nodes(0)?.name())
h.assert_eq[String]("item", nodes(3)?.name())
@@ -965,7 +965,7 @@ class \nodoc\ iso TestXPathResultPostFreeAccess is UnitTest
// across subsequent allocations.
var i: USize = 0
while i < 200 do
- let again = doc.xpathEvalNodes("//item")?
+ let again = doc.xpathEvalNodes("//item") as Array[Xml2Node]
h.assert_eq[USize](4, again.size())
h.assert_eq[String]("item", again(0)?.name())
i = i + 1
@@ -976,12 +976,12 @@ class \nodoc\ iso TestXPathResultPostFreeAccess is UnitTest
// 3. String result path: stringval is owned by the XPath object and
// is freed when the object is freed. The clone must have happened
// before the free.
- let s1: String = doc.xpathEvalString("string(//item[@id='b'])")?
+ let s1: String = doc.xpathEvalString("string(//item[@id='b'])") as String val
h.assert_eq[String]("beta", s1)
// Run repeatedly to surface any free-before-clone regression.
var j: USize = 0
while j < 200 do
- let s = doc.xpathEvalString("string(//item[@id='c'])")?
+ let s = doc.xpathEvalString("string(//item[@id='c'])") as String val
h.assert_eq[String]("gamma", s)
j = j + 1
end
@@ -991,8 +991,8 @@ class \nodoc\ iso TestXPathResultPostFreeAccess is UnitTest
// 4. Scalar paths (Bool, F64): no buffer to free, but exercise the
// same code path to ensure the unconditional free at the end of
// Xml2XPathObject.apply doesn't crash on these branches.
- h.assert_eq[F64](4.0, doc.xpathEvalF64("count(//item)")?)
- h.assert_eq[Bool](true, doc.xpathEvalBool("count(//item) = 4")?)
+ h.assert_eq[F64](4.0, doc.xpathEvalF64("count(//item)") as F64)
+ h.assert_eq[Bool](true, doc.xpathEvalBool("count(//item) = 4") as Bool)
else
h.fail("Failed to exercise post-free XPath access")
end
@@ -1018,7 +1018,7 @@ class \nodoc\ iso TestNodeDumpRepeatedCalls is UnitTest
let xml =
"- alpha
- beta
"
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let children = root.getChildren()
h.assert_eq[USize](2, children.size())
@@ -1068,7 +1068,7 @@ class \nodoc\ iso TestNamespaceUriAndPrefix is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let repository = doc.getRootElement()?
// Root is in the default namespace - URI populated, prefix empty.
h.assert_eq[String](
@@ -1129,7 +1129,7 @@ class \nodoc\ iso TestQname is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let kids = root.getChildren()
@@ -1168,7 +1168,7 @@ class \nodoc\ iso TestGetPropNs is UnitTest
"""
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let field = root.getChildren()(0)?
@@ -1274,7 +1274,7 @@ class \nodoc\ iso TestParseDocNoBlanks is UnitTest
// Default: blank text nodes preserved. getChildren() returns
// element-only children, so we can't distinguish via that API
// alone; instead, serialize and look for the indentation.
- let doc_default = Xml2Doc.parseDoc(xml)?
+ let doc_default = Xml2Parser.parseDoc(xml) as Xml2Doc
let root_default = doc_default.getRootElement()?
let dump_default = root_default.nodeDump(0, 0)
// With blanks preserved, nodeDump output includes the original
@@ -1284,7 +1284,7 @@ class \nodoc\ iso TestParseDocNoBlanks is UnitTest
// With no_blanks: ignorable whitespace dropped during parse.
// The serialized form has no inter-element whitespace.
let opts = Xml2ParserOptions.create(where no_blanks' = true)
- let doc_no_blanks = Xml2Doc.parseDoc(xml, opts)?
+ let doc_no_blanks = Xml2Parser.parseDoc(xml, opts) as Xml2Doc
let root_no_blanks = doc_no_blanks.getRootElement()?
let dump_no_blanks = root_no_blanks.nodeDump(0, 0)
h.assert_eq[String](
@@ -1306,7 +1306,7 @@ class \nodoc\ iso TestParseDocErrorRecovery is UnitTest
// Default: strict parse fails on this input.
try
- let _ = Xml2Doc.parseDoc(malformed)?
+ let _ = Xml2Parser.parseDoc(malformed) as Xml2Doc
h.fail("Default parse should have rejected malformed XML")
end
@@ -1314,7 +1314,7 @@ class \nodoc\ iso TestParseDocErrorRecovery is UnitTest
// libxml2 reconstructed as best it could.
let opts = Xml2ParserOptions.create(where error_recovery' = true)
try
- let doc = Xml2Doc.parseDoc(malformed, opts)?
+ let doc = Xml2Parser.parseDoc(malformed, opts) as Xml2Doc
let root = doc.getRootElement()?
// Whatever libxml2 recovers, the doc has a usable root.
h.assert_eq[String]("root", root.name())
@@ -1341,7 +1341,7 @@ class \nodoc\ iso TestParseDocEntitiesNotSubstitutedByDefault is UnitTest
// Default: entity reference preserved.
try
- let doc = Xml2Doc.parseDoc(xml)?
+ let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let foo = doc.getRootElement()?
let dump = foo.nodeDump(0, 0)
h.assert_true(
@@ -1358,7 +1358,7 @@ class \nodoc\ iso TestParseDocEntitiesNotSubstitutedByDefault is UnitTest
let opts = Xml2ParserOptions.create(
where substitute_entities' = true)
try
- let doc = Xml2Doc.parseDoc(xml, opts)?
+ let doc = Xml2Parser.parseDoc(xml, opts) as Xml2Doc
let foo = doc.getRootElement()?
let dump = foo.nodeDump(0, 0)
h.assert_false(
diff --git a/libxml2/_tests/error_tests.pony b/libxml2/_tests/error_tests.pony
index 2304e33..2d14b03 100644
--- a/libxml2/_tests/error_tests.pony
+++ b/libxml2/_tests/error_tests.pony
@@ -3,6 +3,13 @@ use "pony_test"
use "files"
class \nodoc\ iso TestParseError is UnitTest
+ """
+ Parse a deliberately malformed document and verify the returned
+ `Xml2Error` carries useful structured information. Each assertion
+ is loose enough to survive libxml2 version drift on the exact
+ diagnostic, but tight enough to fail if a regression replaces the
+ rich error with a placeholder.
+ """
fun name(): String => "xml2doc/parse-error"
fun apply(h: TestHelper) =>
@@ -13,19 +20,84 @@ class \nodoc\ iso TestParseError is UnitTest
world
"""
- try
- let doc = Xml2Doc.parseDoc(xml)?
- h.fail("It did not throw an error on bad parse")
- else
- let err: Xml2Error = Xml2Error
- /*
- * Unfortunately, these tests are non-deterministic as the error that is
- * grabbed could be one of many. Roll on later versions with callbacks!
- */
-// h.assert_eq[String](err.domain, "PARSER")
-// h.assert_eq[I32](err.code, 5)
-// h.assert_eq[String](err.level, "Fatal")
-// h.assert_eq[I32](err.line, 3)
-// h.assert_eq[String](
-// err.message, "Extra content at the end of the document\n")
+ match Xml2Parser.parseDoc(xml)
+ | let _: Xml2Doc =>
+ h.fail("malformed XML should not have parsed successfully")
+ | let err: Xml2Error =>
+ // Domain should be the parser - this is a parse error.
+ h.assert_true(
+ err.domain is Xml2ErrorDomainParser,
+ "expected domain=Parser, got domain=" + err.domain.string())
+ // Level should signal an actual problem (Error or Fatal).
+ let level_is_real_error =
+ (err.level is Xml2ErrorLevelError) or
+ (err.level is Xml2ErrorLevelFatal)
+ h.assert_true(
+ level_is_real_error,
+ "expected level Error or Fatal, got " + err.level.string())
+ // libxml2 always assigns a non-zero parser error code.
+ h.assert_ne[I32](I32(0), err.code)
+ // Message should be non-empty.
+ h.assert_true(
+ err.message.size() > 0,
+ "expected non-empty error message")
+ // Line should point inside the input (lines 1-4).
+ h.assert_true(
+ (err.line >= I32(1)) and (err.line <= I32(5)),
+ "line out of range: " + err.line.string())
end
+
+class \nodoc\ iso TestXml2ErrorString is UnitTest
+ """
+ `Xml2Error.string()` should produce a non-empty, human-readable
+ representation containing the level and domain names.
+ """
+ fun name(): String => "xml2error/string-rendering"
+
+ fun apply(h: TestHelper) =>
+ let xml = ""
+ match Xml2Parser.parseDoc(xml)
+ | let _: Xml2Doc => h.fail("malformed XML parsed unexpectedly")
+ | let err: Xml2Error =>
+ let s = err.string()
+ h.assert_true(
+ s.contains("Parser"),
+ "string() output missing domain name: " + s)
+ h.assert_true(
+ (s.contains("Error") or s.contains("Fatal")),
+ "string() output missing level name: " + s)
+ h.assert_true(
+ s.size() > 0,
+ "string() output should be non-empty")
+ end
+
+class \nodoc\ iso TestXml2ErrorDomainFromI32 is UnitTest
+ """
+ Verify the libxml2 domain-code → `Xml2ErrorDomain` mapping covers
+ the documented enum and falls through to Unknown for out-of-range
+ values.
+ """
+ fun name(): String => "xml2error/domain-mapping"
+
+ fun apply(h: TestHelper) =>
+ h.assert_true(Xml2ErrorDomainFromI32(0) is Xml2ErrorDomainNone)
+ h.assert_true(Xml2ErrorDomainFromI32(1) is Xml2ErrorDomainParser)
+ h.assert_true(Xml2ErrorDomainFromI32(8) is Xml2ErrorDomainIo)
+ h.assert_true(Xml2ErrorDomainFromI32(12) is Xml2ErrorDomainXPath)
+ h.assert_true(Xml2ErrorDomainFromI32(30) is Xml2ErrorDomainUri)
+ // Out-of-range falls through.
+ h.assert_true(Xml2ErrorDomainFromI32(99) is Xml2ErrorDomainUnknown)
+ h.assert_true(Xml2ErrorDomainFromI32(-1) is Xml2ErrorDomainUnknown)
+
+class \nodoc\ iso TestXml2ErrorLevelFromI32 is UnitTest
+ """
+ Verify the libxml2 level-code → `Xml2ErrorLevel` mapping.
+ """
+ fun name(): String => "xml2error/level-mapping"
+
+ fun apply(h: TestHelper) =>
+ h.assert_true(Xml2ErrorLevelFromI32(0) is Xml2ErrorLevelNone)
+ h.assert_true(Xml2ErrorLevelFromI32(1) is Xml2ErrorLevelWarning)
+ h.assert_true(Xml2ErrorLevelFromI32(2) is Xml2ErrorLevelError)
+ h.assert_true(Xml2ErrorLevelFromI32(3) is Xml2ErrorLevelFatal)
+ h.assert_true(Xml2ErrorLevelFromI32(99) is Xml2ErrorLevelUnknown)
diff --git a/libxml2/_tests/fuzz_tests.pony b/libxml2/_tests/fuzz_tests.pony
index 0f11b42..420b724 100644
--- a/libxml2/_tests/fuzz_tests.pony
+++ b/libxml2/_tests/fuzz_tests.pony
@@ -131,16 +131,16 @@ class \nodoc\ iso FuzzXPathExpression is Property1[String]
fun ref property(arg1: String, h: PropertyHelper) =>
h.assert_no_error({() ? =>
- let doc = Xml2Doc.parseDoc(
- "onetwo")?
+ let doc = Xml2Parser.parseDoc(
+ "onetwo") as Xml2Doc
// Non-partial form must not crash regardless of input.
let _ = doc.xpathEval(arg1)
// Partial convenience forms may raise on invalid expressions.
try
- let _ = doc.xpathEvalNodes(arg1)?
+ let _ = doc.xpathEvalNodes(arg1) as Array[Xml2Node]
end
try
- let _ = doc.xpathEvalString(arg1)?
+ let _ = doc.xpathEvalString(arg1) as String val
end
} box)
diff --git a/libxml2/_tests/xpath.pony b/libxml2/_tests/xpath.pony
index 1d553b9..06a6242 100755
--- a/libxml2/_tests/xpath.pony
+++ b/libxml2/_tests/xpath.pony
@@ -10,7 +10,7 @@ class \nodoc\ iso TestXPath is UnitTest
try
let xmlbad: Xml2Doc =
- Xml2Doc.parseFile(FileAuth(h.env.root), "Idontexist")?
+ Xml2Parser.parseFile(FileAuth(h.env.root), "Idontexist") as Xml2Doc
h.assert_true(false) // Fail test if I execute here
end
@@ -19,8 +19,9 @@ class \nodoc\ iso TestXPath is UnitTest
.>push(("gi", "http://www.gtk.org/introspection/core/1.0"))
.>push(( "c", "http://www.gtk.org/introspection/c/1.0"))
let xmldoc: Xml2Doc =
- Xml2Doc.parseFile(
- FileAuth(h.env.root), "./libxml2/_tests/libxml2-2.0.gir")?
+ Xml2Parser.parseFile(
+ FileAuth(h.env.root),
+ "./libxml2/_tests/libxml2-2.0.gir") as Xml2Doc
h.assert_eq[USize](
(xmldoc.xpathEval("//*", ns) as Array[Xml2Node]).size(), 15)
h.assert_eq[USize](
diff --git a/libxml2/xml2doc.pony b/libxml2/xml2doc.pony
index 3865092..803b88e 100644
--- a/libxml2/xml2doc.pony
+++ b/libxml2/xml2doc.pony
@@ -29,53 +29,15 @@ class Xml2Doc
"""
let ptr': NullablePointer[XmlDoc]
- new parseFile(
- auth: FileAuth,
- pfilename: String val,
- options: Xml2ParserOptions val = Xml2ParserOptions.create()) ?
- =>
+ new _from_ptr(p: NullablePointer[XmlDoc]) =>
"""
- Parse an XML document from the given file path using libxml2.
-
- - `auth`: Capability proving the caller has permission to read files.
- - `pfilename`: Path to the XML file to parse.
- - `options`: Parser options controlling network access, entity
- expansion, whitespace handling, and recovery behaviour. Defaults
- to a safe-by-default `Xml2ParserOptions` (no network access, no
- entity substitution). See `Xml2ParserOptions` for the full list.
-
- Routes through libxml2's `xmlReadFile`, which is the
- options-aware replacement for the legacy `xmlParseFile`. On
- success, stores the underlying `xmlDoc*` in `ptr'`. Raises an
- error if parsing fails or returns a null document pointer.
+ Package-private constructor used by `Xml2Parser` to wrap a
+ libxml2-allocated `xmlDoc*` (returned by `xmlReadDoc` /
+ `xmlReadFile`) into an `Xml2Doc` instance. The caller is
+ responsible for null-checking `p` before construction; this
+ constructor accepts the pointer as-is.
"""
- let ptrx: NullablePointer[XmlDoc] =
- LibXML2.xmlReadFile(pfilename, "", options.to_flags())
- if ptrx.is_none() then error end
- ptr' = ptrx
-
- new parseDoc(
- pcur: String val,
- options: Xml2ParserOptions val = Xml2ParserOptions.create()) ?
- =>
- """
- Parse an XML document from an in-memory string using libxml2.
-
- - `pcur`: String containing the complete XML document.
- - `options`: Parser options controlling network access, entity
- expansion, whitespace handling, and recovery behaviour. Defaults
- to a safe-by-default `Xml2ParserOptions` (no network access, no
- entity substitution). See `Xml2ParserOptions` for the full list.
-
- Routes through libxml2's `xmlReadDoc`, which is the options-aware
- replacement for the legacy `xmlParseDoc`. On success, stores the
- underlying `xmlDoc*` in `ptr'`. Raises an error if parsing fails
- or returns a null document pointer.
- """
- let ptrx: NullablePointer[XmlDoc] =
- LibXML2.xmlReadDoc(pcur, "", "", options.to_flags())
- if ptrx.is_none() then error end
- ptr' = ptrx
+ ptr' = p
new create(version: String = "1.0") ? =>
"""
@@ -148,9 +110,12 @@ class Xml2Doc
let tmpctx: NullablePointer[XmlXPathContext] =
LibXML2.xmlXPathNewContext(ptr')
if tmpctx.is_none() then
- // Context allocation failed (OOM). Passing a null context to
- // xmlXPathRegisterNs / xmlXPathEval would deref null inside libxml2.
- return None
+ // Context allocation failed (OOM).
+ return Xml2Error._synthetic(
+ Xml2ErrorDomainXPath,
+ Xml2ErrorLevelFatal,
+ I32(-1),
+ "xmlXPathNewContext returned null (OOM)")
end
for (n, url) in namespaces.values() do
LibXML2.xmlXPathRegisterNs(tmpctx, n, url)
@@ -164,43 +129,55 @@ class Xml2Doc
fun xpathEvalNodes(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : Array[Xml2Node] ?
+ : (Array[Xml2Node] | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns an Array[Xml2Node].
+ Convenience method that calls `xpathEval` and projects the result
+ to a nodeset.
+
+ Returns the matched nodes (possibly an empty array if the
+ expression yielded no matches), or an `Xml2Error` if either the
+ evaluation itself failed or the expression evaluated to a non-
+ nodeset value (boolean, number, string).
"""
- (xpathEval(xpath, namespaces) as Array[Xml2Node])
+ _XPathExpect.nodes(xpathEval(xpath, namespaces))
fun xpathEvalString(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : String val ?
+ : (String val | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns a String val.
+ Convenience method that calls `xpathEval` and projects the result
+ to a string. Returns an `Xml2Error` if the evaluation failed or
+ yielded a non-string value.
"""
- (xpathEval(xpath, namespaces) as String val)
+ _XPathExpect.string(xpathEval(xpath, namespaces))
fun xpathEvalF64(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : F64 ?
+ : (F64 | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns an F64 (XML's
- default number type in libxml2).
+ Convenience method that calls `xpathEval` and projects the result
+ to an `F64` (libxml2's number representation). Returns an
+ `Xml2Error` if the evaluation failed or yielded a non-numeric
+ value.
"""
- (xpathEval(xpath, namespaces) as F64)
+ _XPathExpect.f64(xpathEval(xpath, namespaces))
fun xpathEvalBool(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : Bool ?
+ : (Bool | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns a Bool.
+ Convenience method that calls `xpathEval` and projects the result
+ to a `Bool`. Returns an `Xml2Error` if the evaluation failed or
+ yielded a non-boolean value.
"""
- (xpathEval(xpath, namespaces) as Bool)
+ _XPathExpect.bool(xpathEval(xpath, namespaces))
fun ref getRootElement(): Xml2Node ? =>
"""
@@ -317,7 +294,7 @@ class Xml2Doc
Example:
```pony
- let doc = Xml2Doc.parseDoc("text")?
+ let doc = Xml2Parser.parseDoc("text") as Xml2Doc
let xml_string = doc.serialize()? // Pretty-printed UTF-8
let compact = doc.serialize(false)? // Compact output
```
@@ -386,7 +363,7 @@ class Xml2Doc
Example:
```pony
- let doc = Xml2Doc.parseDoc("text")?
+ let doc = Xml2Parser.parseDoc("text") as Xml2Doc
doc.saveToFile(auth, "output.xml")? // Pretty-printed UTF-8
doc.saveToFile(auth, "compact.xml", false, "ISO-8859-1")?
```
diff --git a/libxml2/xml2error.pony b/libxml2/xml2error.pony
index 7e34825..b7944c4 100644
--- a/libxml2/xml2error.pony
+++ b/libxml2/xml2error.pony
@@ -1,85 +1,127 @@
use "raw/"
-class Xml2Error
- var domain: String val
- var code: I32 = I32(0)
- var message: String val
- var level: String val
- var file: String val
- var line: I32 = I32(0)
- var str1: String val
- var str2: String val
- var str3: String val
- var int1: I32 = I32(0)
- var int2: I32 = I32(0)
+class val Xml2Error
+ """
+ Immutable snapshot of an error reported by libxml2.
- new create() =>
- let xmlerrp: NullablePointer[XmlError] = LibXML2.xmlGetLastError()
- try
- let xmlerr: XmlError = xmlerrp.apply()?
- domain =
- match xmlerr.domain
- | 0 => "NONE"
- | 1 => "PARSER"
- | 2 => "TREE"
- | 3 => "NAMESPACE"
- | 4 => "DTD"
- | 5 => "HTML"
- | 6 => "MEMORY"
- | 7 => "OUTPUT"
- | 8 => "IO"
- | 9 => "FTP"
- | 10 => "HTTP"
- | 11 => "XINCLUDE"
- | 12 => "XPATH"
- | 13 => "XPOINTER"
- | 14 => "REGEXP"
- | 15 => "DATATYPE"
- | 16 => "SCHEMASP"
- | 17 => "SCHEMASV"
- | 18 => "RELAXNGP"
- | 19 => "RELAXNGV"
- | 20 => "CATALOG"
- | 21 => "C14N"
- | 22 => "XSLT"
- | 23 => "VALID"
- | 24 => "CHECK"
- | 25 => "WRITER"
- | 26 => "MODULE"
- | 27 => "I18N"
- | 28 => "SCHEMATRONV"
- | 29 => "BUFFER"
- | 30 => "URI"
- else
- "Unknown"
- end
+ An `Xml2Error` is normally constructed via
+ `Xml2Error.from_last_error()?`, which reads libxml2's current
+ per-thread last-error and returns an immutable value. The
+ constructor is partial: it raises if libxml2 has no current error
+ (e.g. no parse / XPath call has failed since the last reset).
- code = xmlerr.code
- message = String.from_cstring(xmlerr.message).clone()
- level =
- match xmlerr.level
- | 0 => "None"
- | 1 => "Warning"
- | 2 => "Error"
- | 3 => "Fatal"
- else
- "Unknown"
- end
- file = String.from_cstring(xmlerr.file).clone()
- line = xmlerr.line
- str1 = String.from_cstring(xmlerr.str1).clone()
- str2 = String.from_cstring(xmlerr.str2).clone()
- str3 = String.from_cstring(xmlerr.str3).clone()
- int1 = xmlerr.int1
- int2 = xmlerr.int2
- else
- message = "XmlError failed to get an error"
- domain = "XmlError failed to get an error"
- file = "XmlError failed to get an error"
- level = "XmlError failed to get an error"
- str1 = "XmlError failed to get an error"
- str2 = "XmlError failed to get an error"
- str3 = "XmlError failed to get an error"
- end
+ All fields are `let`, so an `Xml2Error` value can be safely shared
+ across actors and stored without aliasing concerns.
+
+ ## Thread-locality contract
+
+ libxml2's last-error state is per-OS-thread. Pony actors can
+ migrate between scheduler threads across behaviour boundaries.
+ Construct `Xml2Error.from_last_error()?` in the **same behaviour**
+ as the call that produced the error — typically immediately after
+ catching an error from a parse / XPath operation. Stashing the
+ failure condition and constructing the error later may read stale
+ or unrelated error state.
+
+ The `Xml2Parser.parseDoc` / `parseFile` factories capture errors
+ internally before returning, so callers receiving an `Xml2Error`
+ from those entry points don't need to worry about this contract.
+ """
+
+ // Core
+ let domain: Xml2ErrorDomain
+ let level: Xml2ErrorLevel
+ let code: I32
+ let message: String val
+ let file: String val
+ let line: I32
+
+ // Context strings - libxml2 populates these per-domain (often
+ // tag names, attribute names, file paths).
+ let str1: String val
+ let str2: String val
+ let str3: String val
+
+ // Context integers - libxml2 populates these per-domain (often
+ // line / column numbers).
+ let int1: I32
+ let int2: I32
+
+ new val from_last_error() ? =>
+ """
+ Construct from libxml2's current per-thread last-error. Raises if
+ libxml2 has no current error (i.e. `xmlGetLastError` returned
+ null). The last-error state is reset after a successful read, so
+ a second call without an intervening failure raises.
+ """
+ let xmlerrp: NullablePointer[XmlError] = LibXML2.xmlGetLastError()
+ let xmlerr: XmlError = xmlerrp.apply()?
+ domain = Xml2ErrorDomainFromI32(xmlerr.domain)
+ level = Xml2ErrorLevelFromI32(xmlerr.level)
+ code = xmlerr.code
+ message = _safe_from_cstring(xmlerr.message)
+ file = _safe_from_cstring(xmlerr.file)
+ line = xmlerr.line
+ str1 = _safe_from_cstring(xmlerr.str1)
+ str2 = _safe_from_cstring(xmlerr.str2)
+ str3 = _safe_from_cstring(xmlerr.str3)
+ int1 = xmlerr.int1
+ int2 = xmlerr.int2
LibXML2.xmlResetError(xmlerrp)
+ new val _synthetic(
+ domain': Xml2ErrorDomain,
+ level': Xml2ErrorLevel,
+ code': I32,
+ message': String val)
+ =>
+ """
+ Package-private constructor for synthetic errors that did not come
+ from libxml2's last-error state. Used by `Xml2Parser` as a
+ fallback for the impossible case where libxml2 returns a null
+ doc pointer with no captured error.
+ """
+ domain = domain'
+ level = level'
+ code = code'
+ message = message'
+ file = ""
+ line = -1
+ str1 = ""
+ str2 = ""
+ str3 = ""
+ int1 = 0
+ int2 = 0
+
+ fun tag _safe_from_cstring(p: Pointer[U8]): String val =>
+ """
+ Defensive `String.from_cstring` that returns an empty Pony string
+ for a null pointer. libxml2 leaves several `xmlError` string
+ fields NULL depending on the error domain.
+ """
+ String.from_cstring(p).clone()
+
+ fun string(): String val =>
+ """
+ Human-readable rendering of the error, suitable for logging.
+ Format: `" [] at :: "`,
+ with `:` omitted if no file is known.
+ """
+ let buf: String iso = recover iso String end
+ buf.append(level.string())
+ buf.append(" ")
+ buf.append(domain.string())
+ buf.append("[")
+ buf.append(code.string())
+ buf.append("]")
+ if file.size() > 0 then
+ buf.append(" at ")
+ buf.append(file)
+ buf.append(":")
+ buf.append(line.string())
+ end
+ if message.size() > 0 then
+ buf.append(": ")
+ buf.append(message)
+ end
+ consume buf
diff --git a/libxml2/xml2errordomain.pony b/libxml2/xml2errordomain.pony
new file mode 100644
index 0000000..5459972
--- /dev/null
+++ b/libxml2/xml2errordomain.pony
@@ -0,0 +1,146 @@
+type Xml2ErrorDomain is
+ ( Xml2ErrorDomainNone
+ | Xml2ErrorDomainParser
+ | Xml2ErrorDomainTree
+ | Xml2ErrorDomainNamespace
+ | Xml2ErrorDomainDtd
+ | Xml2ErrorDomainHtml
+ | Xml2ErrorDomainMemory
+ | Xml2ErrorDomainOutput
+ | Xml2ErrorDomainIo
+ | Xml2ErrorDomainFtp
+ | Xml2ErrorDomainHttp
+ | Xml2ErrorDomainXInclude
+ | Xml2ErrorDomainXPath
+ | Xml2ErrorDomainXPointer
+ | Xml2ErrorDomainRegexp
+ | Xml2ErrorDomainDatatype
+ | Xml2ErrorDomainSchemasParser
+ | Xml2ErrorDomainSchemasValid
+ | Xml2ErrorDomainRelaxNgParser
+ | Xml2ErrorDomainRelaxNgValid
+ | Xml2ErrorDomainCatalog
+ | Xml2ErrorDomainC14n
+ | Xml2ErrorDomainXslt
+ | Xml2ErrorDomainValid
+ | Xml2ErrorDomainCheck
+ | Xml2ErrorDomainWriter
+ | Xml2ErrorDomainModule
+ | Xml2ErrorDomainI18n
+ | Xml2ErrorDomainSchematronValid
+ | Xml2ErrorDomainBuffer
+ | Xml2ErrorDomainUri
+ | Xml2ErrorDomainUnknown )
+ """
+ Subsystem of libxml2 that produced an `Xml2Error`. Mirrors libxml2's
+ `xmlErrorDomain` enum (values 0–30). `Xml2ErrorDomainUnknown` is the
+ fall-through for values outside the documented range.
+ """
+
+primitive Xml2ErrorDomainNone
+ fun string(): String iso^ => "None".clone()
+primitive Xml2ErrorDomainParser
+ fun string(): String iso^ => "Parser".clone()
+primitive Xml2ErrorDomainTree
+ fun string(): String iso^ => "Tree".clone()
+primitive Xml2ErrorDomainNamespace
+ fun string(): String iso^ => "Namespace".clone()
+primitive Xml2ErrorDomainDtd
+ fun string(): String iso^ => "Dtd".clone()
+primitive Xml2ErrorDomainHtml
+ fun string(): String iso^ => "Html".clone()
+primitive Xml2ErrorDomainMemory
+ fun string(): String iso^ => "Memory".clone()
+primitive Xml2ErrorDomainOutput
+ fun string(): String iso^ => "Output".clone()
+primitive Xml2ErrorDomainIo
+ fun string(): String iso^ => "Io".clone()
+primitive Xml2ErrorDomainFtp
+ fun string(): String iso^ => "Ftp".clone()
+primitive Xml2ErrorDomainHttp
+ fun string(): String iso^ => "Http".clone()
+primitive Xml2ErrorDomainXInclude
+ fun string(): String iso^ => "XInclude".clone()
+primitive Xml2ErrorDomainXPath
+ fun string(): String iso^ => "XPath".clone()
+primitive Xml2ErrorDomainXPointer
+ fun string(): String iso^ => "XPointer".clone()
+primitive Xml2ErrorDomainRegexp
+ fun string(): String iso^ => "Regexp".clone()
+primitive Xml2ErrorDomainDatatype
+ fun string(): String iso^ => "Datatype".clone()
+primitive Xml2ErrorDomainSchemasParser
+ fun string(): String iso^ => "SchemasParser".clone()
+primitive Xml2ErrorDomainSchemasValid
+ fun string(): String iso^ => "SchemasValid".clone()
+primitive Xml2ErrorDomainRelaxNgParser
+ fun string(): String iso^ => "RelaxNgParser".clone()
+primitive Xml2ErrorDomainRelaxNgValid
+ fun string(): String iso^ => "RelaxNgValid".clone()
+primitive Xml2ErrorDomainCatalog
+ fun string(): String iso^ => "Catalog".clone()
+primitive Xml2ErrorDomainC14n
+ fun string(): String iso^ => "C14n".clone()
+primitive Xml2ErrorDomainXslt
+ fun string(): String iso^ => "Xslt".clone()
+primitive Xml2ErrorDomainValid
+ fun string(): String iso^ => "Valid".clone()
+primitive Xml2ErrorDomainCheck
+ fun string(): String iso^ => "Check".clone()
+primitive Xml2ErrorDomainWriter
+ fun string(): String iso^ => "Writer".clone()
+primitive Xml2ErrorDomainModule
+ fun string(): String iso^ => "Module".clone()
+primitive Xml2ErrorDomainI18n
+ fun string(): String iso^ => "I18n".clone()
+primitive Xml2ErrorDomainSchematronValid
+ fun string(): String iso^ => "SchematronValid".clone()
+primitive Xml2ErrorDomainBuffer
+ fun string(): String iso^ => "Buffer".clone()
+primitive Xml2ErrorDomainUri
+ fun string(): String iso^ => "Uri".clone()
+primitive Xml2ErrorDomainUnknown
+ fun string(): String iso^ => "Unknown".clone()
+
+primitive Xml2ErrorDomainFromI32
+ """
+ Convert a libxml2 `xmlErrorDomain` integer code to its corresponding
+ `Xml2ErrorDomain` primitive. Out-of-range values map to
+ `Xml2ErrorDomainUnknown`.
+ """
+ fun apply(domain: I32): Xml2ErrorDomain =>
+ match domain
+ | 0 => Xml2ErrorDomainNone
+ | 1 => Xml2ErrorDomainParser
+ | 2 => Xml2ErrorDomainTree
+ | 3 => Xml2ErrorDomainNamespace
+ | 4 => Xml2ErrorDomainDtd
+ | 5 => Xml2ErrorDomainHtml
+ | 6 => Xml2ErrorDomainMemory
+ | 7 => Xml2ErrorDomainOutput
+ | 8 => Xml2ErrorDomainIo
+ | 9 => Xml2ErrorDomainFtp
+ | 10 => Xml2ErrorDomainHttp
+ | 11 => Xml2ErrorDomainXInclude
+ | 12 => Xml2ErrorDomainXPath
+ | 13 => Xml2ErrorDomainXPointer
+ | 14 => Xml2ErrorDomainRegexp
+ | 15 => Xml2ErrorDomainDatatype
+ | 16 => Xml2ErrorDomainSchemasParser
+ | 17 => Xml2ErrorDomainSchemasValid
+ | 18 => Xml2ErrorDomainRelaxNgParser
+ | 19 => Xml2ErrorDomainRelaxNgValid
+ | 20 => Xml2ErrorDomainCatalog
+ | 21 => Xml2ErrorDomainC14n
+ | 22 => Xml2ErrorDomainXslt
+ | 23 => Xml2ErrorDomainValid
+ | 24 => Xml2ErrorDomainCheck
+ | 25 => Xml2ErrorDomainWriter
+ | 26 => Xml2ErrorDomainModule
+ | 27 => Xml2ErrorDomainI18n
+ | 28 => Xml2ErrorDomainSchematronValid
+ | 29 => Xml2ErrorDomainBuffer
+ | 30 => Xml2ErrorDomainUri
+ else
+ Xml2ErrorDomainUnknown
+ end
diff --git a/libxml2/xml2errorlevel.pony b/libxml2/xml2errorlevel.pony
new file mode 100644
index 0000000..31c6404
--- /dev/null
+++ b/libxml2/xml2errorlevel.pony
@@ -0,0 +1,44 @@
+type Xml2ErrorLevel is
+ ( Xml2ErrorLevelNone
+ | Xml2ErrorLevelWarning
+ | Xml2ErrorLevelError
+ | Xml2ErrorLevelFatal
+ | Xml2ErrorLevelUnknown )
+ """
+ Severity level of an `Xml2Error`. Mirrors libxml2's
+ `xmlErrorLevel` enum.
+
+ - `Xml2ErrorLevelNone` (libxml2 value 0): not an error
+ - `Xml2ErrorLevelWarning` (1): a simple warning, parsing can continue
+ - `Xml2ErrorLevelError` (2): a recoverable error
+ - `Xml2ErrorLevelFatal` (3): a fatal error, parsing cannot continue
+ - `Xml2ErrorLevelUnknown`: libxml2 reported a level outside the
+ documented enum range
+ """
+
+primitive Xml2ErrorLevelNone
+ fun string(): String iso^ => "None".clone()
+primitive Xml2ErrorLevelWarning
+ fun string(): String iso^ => "Warning".clone()
+primitive Xml2ErrorLevelError
+ fun string(): String iso^ => "Error".clone()
+primitive Xml2ErrorLevelFatal
+ fun string(): String iso^ => "Fatal".clone()
+primitive Xml2ErrorLevelUnknown
+ fun string(): String iso^ => "Unknown".clone()
+
+primitive Xml2ErrorLevelFromI32
+ """
+ Convert a libxml2 `xmlErrorLevel` integer code to its corresponding
+ `Xml2ErrorLevel` primitive. Out-of-range values map to
+ `Xml2ErrorLevelUnknown`.
+ """
+ fun apply(level: I32): Xml2ErrorLevel =>
+ match level
+ | 0 => Xml2ErrorLevelNone
+ | 1 => Xml2ErrorLevelWarning
+ | 2 => Xml2ErrorLevelError
+ | 3 => Xml2ErrorLevelFatal
+ else
+ Xml2ErrorLevelUnknown
+ end
diff --git a/libxml2/xml2node.pony b/libxml2/xml2node.pony
index 50182ab..11e79b7 100644
--- a/libxml2/xml2node.pony
+++ b/libxml2/xml2node.pony
@@ -48,20 +48,25 @@ class Xml2Node
let tmpctx: NullablePointer[XmlXPathContext] =
LibXML2.xmlXPathNewContext(ptr.doc)
if tmpctx.is_none() then
- // Context allocation failed (OOM). Passing a null context to the
- // calls below would deref null inside libxml2.
- return None
+ return Xml2Error._synthetic(
+ Xml2ErrorDomainXPath,
+ Xml2ErrorLevelFatal,
+ I32(-1),
+ "xmlXPathNewContext returned null (OOM)")
end
for (n, url) in namespaces.values() do
LibXML2.xmlXPathRegisterNs(tmpctx, n, url)
end
if LibXML2.xmlXPathSetContextNode(ptr', tmpctx) < 0 then
- // Failure to set the context node would silently fall back to the
- // document root, so a query like "./child" would return matches
- // from the whole document instead of from this node. Free the
- // context and return None rather than producing wrong results.
+ // Failure to set the context node would silently fall back to
+ // the document root, so a query like "./child" would return
+ // matches from the whole document instead of from this node.
LibXML2.xmlXPathFreeContext(tmpctx)
- return None
+ return Xml2Error._synthetic(
+ Xml2ErrorDomainXPath,
+ Xml2ErrorLevelError,
+ I32(-3),
+ "xmlXPathSetContextNode returned negative")
end
let xptr: NullablePointer[XmlXPathObject] =
LibXML2.xmlXPathEval(xpath, tmpctx)
@@ -72,43 +77,51 @@ class Xml2Node
fun xpathEvalNodes(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : Array[Xml2Node] ?
+ : (Array[Xml2Node] | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns an Array[Xml2Node].
+ Convenience method that calls `xpathEval` and projects the result
+ to a nodeset. Returns the matched nodes (possibly an empty array
+ if no matches) or an `Xml2Error` describing the failure.
"""
- (xpathEval(xpath, namespaces) as Array[Xml2Node])
+ _XPathExpect.nodes(xpathEval(xpath, namespaces))
fun xpathEvalString(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : String val ?
+ : (String val | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns a String val.
+ Convenience method that calls `xpathEval` and projects the result
+ to a string. Returns an `Xml2Error` if the evaluation failed or
+ yielded a non-string value.
"""
- (xpathEval(xpath, namespaces) as String val)
+ _XPathExpect.string(xpathEval(xpath, namespaces))
fun xpathEvalF64(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : F64 ?
+ : (F64 | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns an F64 (XML's
- default Number type in libxml2).
+ Convenience method that calls `xpathEval` and projects the result
+ to an `F64` (libxml2's number representation). Returns an
+ `Xml2Error` if the evaluation failed or yielded a non-numeric
+ value.
"""
- (xpathEval(xpath, namespaces) as F64)
+ _XPathExpect.f64(xpathEval(xpath, namespaces))
fun xpathEvalBool(
xpath: String val,
namespaces: Array[(String val, String val)] = [])
- : Bool ?
+ : (Bool | Xml2Error)
=>
"""
- A convenience method that calls xpathEval and returns a Bool.
+ Convenience method that calls `xpathEval` and projects the result
+ to a `Bool`. Returns an `Xml2Error` if the evaluation failed or
+ yielded a non-boolean value.
"""
- (xpathEval(xpath, namespaces) as Bool)
+ _XPathExpect.bool(xpathEval(xpath, namespaces))
fun ref name(): String val =>
"""
diff --git a/libxml2/xml2parser.pony b/libxml2/xml2parser.pony
new file mode 100644
index 0000000..b3d1de9
--- /dev/null
+++ b/libxml2/xml2parser.pony
@@ -0,0 +1,85 @@
+use "raw/"
+use "files"
+
+primitive Xml2Parser
+ """
+ Entry points for parsing XML documents that return errors as data
+ rather than raising.
+
+ Each factory routes through libxml2's options-aware `xmlRead*`
+ family and returns a union of the parsed `Xml2Doc` and an
+ `Xml2Error` capturing the failure. Errors are constructed
+ immediately in the same Pony behaviour as the parse call, so the
+ thread-locality contract documented on `Xml2Error` is satisfied by
+ default for callers using these entry points.
+
+ ## Example
+
+ ```pony
+ match Xml2Parser.parseDoc(xml)
+ | let doc: Xml2Doc =>
+ let root = doc.getRootElement()?
+ // ...
+ | let err: Xml2Error =>
+ env.err.print("parse failed: " + err.string())
+ end
+ ```
+ """
+
+ fun parseDoc(
+ xml: String val,
+ options: Xml2ParserOptions val = Xml2ParserOptions.create())
+ : (Xml2Doc | Xml2Error)
+ =>
+ """
+ Parse `xml` as an XML document. Returns the parsed `Xml2Doc` on
+ success, or an `Xml2Error` describing the failure.
+ """
+ let ptrx: NullablePointer[XmlDoc] =
+ LibXML2.xmlReadDoc(xml, "", "", options.to_flags())
+ if ptrx.is_none() then
+ _capture_error("xmlReadDoc returned null")
+ else
+ Xml2Doc._from_ptr(ptrx)
+ end
+
+ fun parseFile(
+ auth: FileAuth,
+ path: String val,
+ options: Xml2ParserOptions val = Xml2ParserOptions.create())
+ : (Xml2Doc | Xml2Error)
+ =>
+ """
+ Parse the XML document at `path` from the file system. Returns
+ the parsed `Xml2Doc` on success, or an `Xml2Error` describing the
+ failure.
+
+ `auth` is the `FileAuth` capability indicating the caller is
+ permitted to read files. The path is passed directly to libxml2's
+ `xmlReadFile` without further sandboxing.
+ """
+ let ptrx: NullablePointer[XmlDoc] =
+ LibXML2.xmlReadFile(path, "", options.to_flags())
+ if ptrx.is_none() then
+ _capture_error("xmlReadFile returned null")
+ else
+ Xml2Doc._from_ptr(ptrx)
+ end
+
+ fun tag _capture_error(synthetic_message: String): Xml2Error =>
+ """
+ Read libxml2's last-error and return an `Xml2Error`. If libxml2
+ has no captured error despite the apparent parse failure
+ (extremely rare; usually only on allocation failure inside the
+ read path), return a synthetic error so the public return type
+ stays a total `(Xml2Doc | Xml2Error)`.
+ """
+ try
+ Xml2Error.from_last_error()?
+ else
+ Xml2Error._synthetic(
+ Xml2ErrorDomainUnknown,
+ Xml2ErrorLevelFatal,
+ I32(-1),
+ synthetic_message)
+ end
diff --git a/libxml2/xml2parseroptions.pony b/libxml2/xml2parseroptions.pony
index 610171a..8385107 100644
--- a/libxml2/xml2parseroptions.pony
+++ b/libxml2/xml2parseroptions.pony
@@ -23,8 +23,11 @@ class val Xml2ParserOptions
```pony
let opts = Xml2ParserOptions.create(
- where no_blanks = true, pedantic = true)
- let doc = Xml2Doc.parseDoc(xml, opts)?
+ where no_blanks' = true, pedantic' = true)
+ match Xml2Parser.parseDoc(xml, opts)
+ | let doc: Xml2Doc => // ...
+ | let err: Xml2Error => // ...
+ end
```
"""
@@ -91,8 +94,8 @@ class val Xml2ParserOptions
fun to_flags(): I32 =>
"""
Return the OR'd libxml2 `XML_PARSE_*` bitmask corresponding to
- the enabled options. Consumed internally by `Xml2Doc.parseDoc` /
- `parseFile` when calling `xmlReadDoc` / `xmlReadFile`.
+ the enabled options. Consumed internally by `Xml2Parser.parseDoc`
+ / `parseFile` when calling `xmlReadDoc` / `xmlReadFile`.
"""
var f: I32 = 0
if error_recovery then f = f or 1 end // XML_PARSE_RECOVER = 1<<0
diff --git a/libxml2/xml2xpathexpect.pony b/libxml2/xml2xpathexpect.pony
new file mode 100644
index 0000000..4ea702a
--- /dev/null
+++ b/libxml2/xml2xpathexpect.pony
@@ -0,0 +1,43 @@
+primitive _XPathExpect
+ """
+ Internal helpers shared by `Xml2Doc.xpathEval*` and
+ `Xml2Node.xpathEval*` convenience methods. Projects an
+ `Xml2XPathResult` to a specific variant, returning an `Xml2Error`
+ if the actual variant doesn't match the requested one (or if
+ evaluation itself failed).
+ """
+
+ fun nodes(r: Xml2XPathResult): (Array[Xml2Node] | Xml2Error) =>
+ match r
+ | let a: Array[Xml2Node] => a
+ | let e: Xml2Error => e
+ else _type_mismatch("nodeset")
+ end
+
+ fun string(r: Xml2XPathResult): (String val | Xml2Error) =>
+ match r
+ | let s: String val => s
+ | let e: Xml2Error => e
+ else _type_mismatch("string")
+ end
+
+ fun f64(r: Xml2XPathResult): (F64 | Xml2Error) =>
+ match r
+ | let f: F64 => f
+ | let e: Xml2Error => e
+ else _type_mismatch("number")
+ end
+
+ fun bool(r: Xml2XPathResult): (Bool | Xml2Error) =>
+ match r
+ | let b: Bool => b
+ | let e: Xml2Error => e
+ else _type_mismatch("boolean")
+ end
+
+ fun tag _type_mismatch(expected: String): Xml2Error =>
+ Xml2Error._synthetic(
+ Xml2ErrorDomainXPath,
+ Xml2ErrorLevelError,
+ I32(-2),
+ "xpath result was not a " + expected)
diff --git a/libxml2/xml2xpathobject.pony b/libxml2/xml2xpathobject.pony
index 55d4551..4762674 100644
--- a/libxml2/xml2xpathobject.pony
+++ b/libxml2/xml2xpathobject.pony
@@ -4,7 +4,26 @@ use "debug"
use @pony_triggergc[None](ptr: Pointer[None])
use @pony_ctx[Pointer[None]]()
-type Xml2XPathResult is (None | Array[Xml2Node] | Bool | String val | F64)
+type Xml2XPathResult is
+ (None | Array[Xml2Node] | Bool | String val | F64 | Xml2Error)
+ """
+ Discriminated union returned by `Xml2Doc.xpathEval` and
+ `Xml2Node.xpathEval`.
+
+ Variants:
+
+ - `Array[Xml2Node]` — the expression evaluated to a node-set; the
+ array (possibly empty) contains the matched nodes.
+ - `Bool` — the expression evaluated to a boolean (`boolean(...)`,
+ `count(...) > 0`, etc.).
+ - `F64` — the expression evaluated to a number.
+ - `String val` — the expression evaluated to a string.
+ - `None` — the result type is one libxml2 supports but this binding
+ does not yet wrap (point, range, location-set, users, XSLT tree).
+ Treated as "no useful Pony-side result" rather than as an error.
+ - `Xml2Error` — the evaluation failed (typically a malformed XPath
+ expression). The error captures libxml2's diagnostic data.
+ """
primitive Xml2XPathObject
"""
@@ -17,39 +36,57 @@ primitive Xml2XPathObject
: Xml2XPathResult
=>
"""
- Wrap a nullable `xmlXPathObject*` and return an `Xml2XPathResult`
- matching the XPath result type.
+ Wrap a nullable `xmlXPathObject*` and return an
+ `Xml2XPathResult` matching the XPath result type.
Behaviour:
- - If `ptrx` is `None` or cannot be dereferenced, returns `None`.
- - If the XPath type is `nodeset` and `nodesetval` is non-null, converts
- it into an `Array[Xml2Node]`, one wrapper per node in the set.
+ - If `ptrx` is `None` (evaluation failed — typically a malformed
+ XPath expression), returns an `Xml2Error` captured from
+ libxml2's last-error.
+ - If the XPath type is `nodeset`, converts it into an
+ `Array[Xml2Node]` (empty array if the nodeset is empty rather
+ than `None`).
- If the type is `boolean`, returns a Pony `Bool` (`true` when
`boolval == 1`, otherwise `false`).
- If the type is `number`, returns a Pony `F64` from `floatval`.
- - If the type is `string`, returns an immutable Pony `String` cloned
- from `stringval`.
- - For undefined, point, range, location-set, user, or XSLT-tree types,
- returns `None` (currently unsupported).
+ - If the type is `string`, returns an immutable Pony `String`
+ cloned from `stringval`.
+ - For undefined, point, range, location-set, user, or XSLT-tree
+ types, returns `None` (the eval succeeded but produced a result
+ this binding does not yet wrap).
- This function takes ownership of the underlying `xmlXPathObject*` and
- calls `xmlXPathFreeObject` on it before returning, after snapshotting the
- relevant data into Pony-owned values. Node pointers stored in the
- returned `Array[Xml2Node]` remain valid because they are owned by the
- document (which is kept alive via the `xml2doc tag` reference), not by
- the freed XPath object.
+ This function takes ownership of the underlying
+ `xmlXPathObject*` and calls `xmlXPathFreeObject` on it before
+ returning, after snapshotting the relevant data into Pony-owned
+ values. Node pointers stored in the returned `Array[Xml2Node]`
+ remain valid because they are owned by the document (which is
+ kept alive via the `xml2doc tag` reference), not by the freed
+ XPath object.
"""
+ if ptrx.is_none() then
+ // Evaluation failed - return the captured error. xmlXPathEval
+ // is documented to populate xmlGetLastError on failure.
+ let err: Xml2Error =
+ try Xml2Error.from_last_error()?
+ else
+ Xml2Error._synthetic(
+ Xml2ErrorDomainXPath,
+ Xml2ErrorLevelError,
+ I32(-1),
+ "xpath evaluation returned null with no last-error")
+ end
+ LibXML2.xmlXPathFreeObject(ptrx)
+ return err
+ end
let result: Xml2XPathResult =
try
let ptr: XmlXPathObject = ptrx.apply()?
match ptr.xmltype
| XPathTypeUndefined() => None
| XPathTypeNodeset() =>
- if ptr.nodesetval.is_none() then
- None
- else
- let nodearray: Array[Xml2Node] = Array[Xml2Node]
+ let nodearray: Array[Xml2Node] = Array[Xml2Node]
+ if not ptr.nodesetval.is_none() then
let nodeset: XmlNodeSet = ptr.nodesetval.apply()?
// Borrow `nodeTab` as a Pony Array to iterate, then copy each
// node pointer into a Pony-owned `Array[Xml2Node]`. The node
@@ -62,8 +99,8 @@ primitive Xml2XPathObject
for f in nodearray'.values() do
nodearray.push(Xml2Node.fromPTR(xml2doc, f)?)
end
- nodearray
end
+ nodearray
| XPathTypeBoolean() => (ptr.boolval == 1)
| XPathTypeNumber() => ptr.floatval
| XPathTypeString() =>
@@ -82,7 +119,5 @@ primitive Xml2XPathObject
None
end
// Free the XPath object after all data is snapshotted into Pony memory.
- // `xmlXPathFreeObject` is documented to accept NULL safely, so the case
- // where `ptrx.apply()?` raised (None pointer) is handled implicitly.
LibXML2.xmlXPathFreeObject(ptrx)
result