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 = "alphabeta" 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