Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .release-notes/36.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
3 changes: 3 additions & 0 deletions libxml2/_test.pony
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 22 additions & 22 deletions libxml2/_tests/basic_tests.pony
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class \nodoc\ iso TestModifyProps is UnitTest
</root>
"""
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),
"""
Expand Down Expand Up @@ -59,7 +59,7 @@ class \nodoc\ iso TestGetProps is UnitTest
</root>
"""
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()
Expand Down Expand Up @@ -92,7 +92,7 @@ class \nodoc\ iso TestParseDocAndRoot is UnitTest
</root>
"""
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()
Expand All @@ -114,7 +114,7 @@ class \nodoc\ iso TestDocXPathSimpleNodeset is UnitTest
</root>
"""
try
let doc = Xml2Doc.parseDoc(xml)?
let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let res = doc.xpathEval("//child")
match res
| let nodes: Array[Xml2Node] =>
Expand All @@ -140,8 +140,8 @@ class \nodoc\ iso TestDocXPathSimpleNodesetConvenience is UnitTest
</root>
"""
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())
Expand All @@ -166,7 +166,7 @@ class \nodoc\ iso TestNodeXPathRelative is UnitTest
</root>
"""
try
let doc = Xml2Doc.parseDoc(xml)?
let doc = Xml2Parser.parseDoc(xml) as Xml2Doc
let root = doc.getRootElement()?
let sections = doc.xpathEval("//section")
match sections
Expand Down Expand Up @@ -206,12 +206,12 @@ class \nodoc\ iso TestNodeXPathRelativeConvenience is UnitTest
</root>
"""
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())
Expand All @@ -230,7 +230,7 @@ class \nodoc\ iso TestNodeAttributesAndContent is UnitTest
</root>
"""
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] =>
Expand Down Expand Up @@ -260,7 +260,7 @@ class \nodoc\ iso TestXPathScalarResults is UnitTest
</root>
"""
try
let doc = Xml2Doc.parseDoc(xml)?
let doc = Xml2Parser.parseDoc(xml) as Xml2Doc

// count() -> number
let count_res = doc.xpathEval("count(//child)")
Expand Down Expand Up @@ -304,18 +304,18 @@ class \nodoc\ iso TestXPathScalarResultsConvenience is UnitTest
</root>
"""
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")
Expand Down Expand Up @@ -367,7 +367,7 @@ class \nodoc\ iso TestCreateAndAppendChildren is UnitTest
h.assert_true(xml.contains("<child>World</child>"))

// 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")
Expand Down Expand Up @@ -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")
Expand All @@ -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()
Expand Down
Loading
Loading