8.2.165-stable Switch to dev

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.