xml
XML parsing for GreyCat: a typed element tree, and a reader that streams one repeated element at a time out of a document too large to hold whole.
@library("xml", "0.0.0");
Replaces std’s io::XmlReader, which drops the text preceding a character
entity and can only hand back a whole file as a nested Map.
Usage
Parse a document held in memory, or read one off disk:
var root = XmlElement::parse(body);
var root = XmlElement::load("listing.xml");
println(root.text_of("Name"));
for (_, entry in root.all("Contents")) {
println("${entry.text_of("Key")} ${entry.int_of("Size")}");
}
Stream instead when the document is large. Each read yields one
select-matching subtree, and the reader holds only that subtree:
var reader = xml::XmlReader { path: "listing.xml", select: "Contents" };
while (reader.can_read()) {
println(reader.read().text_of("Key"));
}
select matches an element by local name at any depth. When it is null, each
direct child of the root is yielded.
Deserializing into your own types
Xml<T> and XmlReader<T> map a document onto a GreyCat type, as Json<T>
does for JSON:
type Contents {
Key: String;
Size: int;
LastModified: time?;
Tag: Array<String>;
}
var entry = Xml<Contents> {}.parse(body);
var reader = xml::XmlReader<Contents> { path: "listing.xml", select: "Contents" };
while (reader.can_read()) {
println(reader.read().Key);
}
A field binds to the child element of that name, failing that the attribute of
that name, failing that – for a field named text – the element’s own text.
Names match exactly, with no case conversion. A missing nullable field is null,
a missing non-nullable one throws, and an unknown element is ignored. A field
typed XmlElement takes its subtree unmapped.
Enums, Map<String, V> and @format on a time or duration are supported.
A field typed as an abstract type takes its concrete subtype from the
element’s own name, so a polymorphic child list needs no annotation:
abstract type Shape { id: String?; }
type Circle extends Shape { r: float; }
type Group extends Shape { shapes: Array<Shape>; }
Leaving the parameter off yields the XmlElement tree, but the compiler types
it as any?; write XmlReader<XmlElement> when you want the tree statically
typed.
XmlElement
name, prefix, attributes, text and children are the tree;
child, all, attr, text_of, int_of, float_of, bool_of and time_of
read it. A missing child gives null and an empty one gives "", so the two stay
apart.
Character entities are decoded, CDATA is taken literally, namespace prefixes are split off the name, and text is byte-exact: nothing is trimmed or normalized. An unknown or unterminated reference is reproduced rather than dropped. Malformed input throws, naming the byte offset.
The prolog, comments, processing instructions and the DOCTYPE are skipped. Since text is never transcoded, a document declaring an encoding other than UTF-8, or opening on a UTF-16 byte order mark, is refused rather than handed back as mojibake.
Typed deserialization (Xml<T> {}.parse, mirroring Json<T>) does not exist.
Naming
Until io::XmlReader is removed from std, both libraries export an
XmlReader, and the bare name resolves to std’s. Write xml::XmlReader.
XmlElement is unambiguous and is reached by bare name.