Decoding documents¶
Validation answers is this document allowed? Decoding answers what does it say? — and hands back data rather than an event stream.
use xsdkit::Schemas;
fn read(schemas: &Schemas, xml: &str) -> Option<()> {
let doc = schemas.decode(xml).into_result().ok()?;
for child in doc.children() {
// `display_psvi_name` rather than `local_of`: a child a wildcard
// admitted has no interned name to look up.
println!("{}: {}", schemas.display_psvi_name(&child.name), child.text());
}
Some(())
}
A document is XML text, bytes whose encoding is detected, or a pathlib.Path
to read it from. A str is always content — a path and a document cannot be
told apart once both are strings — so pass a Path for a file:
from pathlib import Path
schemas.decode(Path("report.xml")) # read the file
schemas.decode(Path("report.xml").read_bytes())
schemas.decode("report.xml") # DocumentError: this is not a document
Values arrive in their value space, not as strings to re-parse: a
xs:decimal is a Decimal, a xs:date is a datetime.date, a
xs:positiveInteger is an int. That falls out of decoding a typed PSVI
rather than a parse tree — the conversion already happened during validation.
A decimal keeps the scale it was written with: 4.50 decodes to
Decimal('4.50'), which equals Decimal('4.5') as the schema says it must,
and still prints — and multiplies — the way the document wrote it.
The mapping¶
| XML | Python |
|---|---|
| element with children | dict |
| element with a simple value, no attributes | the value itself |
| element with a value and attributes | dict with the value under "$" |
| attribute | key prefixed with "@" |
xsi:nil="true" |
None, or None under "$" when the element also has attributes |
| character data in mixed content | "$" |
A list is a list¶
A child the schema allows more than once is always a list — holding two entries, one, or none:
schemas = xsdkit.SchemaSet.from_file("report.xsd")
data = schemas.decode(open("report.xml").read())
len(data["item"]) # 2
type(data["item"]).__name__ # 'list'
The shape comes from the schema, not from the document in front of you. That
removes the defect every schema-less XML-to-dict converter has: with
xmltodict, a <report> with one <item> decodes to a dict where a report
with two decodes to a list, so every consumer writes
```python,ignore items = data["item"] items = items if isinstance(items, list) else [items] # not needed here
and the ones that forget break the first time a document has exactly one of
something. Here `data["item"]` is a list in all three cases.
`xmlschema` is schema-aware too, and its default converter gets the one-item
case right as well. Where the two differ is the empty case: an absent
repeating child is `[]` here, where `xmlschema` leaves the key out, so
`data["item"]` needs no `.get("item", [])` even when the document has none.
Keys come in the order each name first appears in the document. Every
occurrence of a name goes into that name's one list, so how *different* names
interleave is not kept: under a repeating choice, `<x>1</x><y>2</y><x>3</x>`
decodes to `{'x': [1, 3], 'y': [2]}`. Where that order carries meaning, read
the [PSVI events](validation.md) instead. A repeating child the document did
not carry has no position at all, so its empty list comes after the keys that
were present.
A child that may appear **at most once** and does not appear has no key at
all, which is how `note` behaves above:
```python
[k for k in schemas.decode(open("report.xml").read())["item"][1]]
# ['@sku', '@quantity', 'price'] — no 'note', it was not there
Names¶
Keys are local names. Clark notation appears only where two names under one
parent would otherwise collide — and whether they collide is decided by the
schema, so a key does not change shape because a particular document left
a sibling out. A child or attribute that a wildcard admitted under a name the
schema never declares is not the schema's to shorten, so it keeps its full
name — "@{urn:other}foo" — whenever it has a namespace.
Namespaces are the one thing the dictionary form gives up. Where that matters,
the Rust Decoded tree keeps every qualified name, the
type in force after any xsi:type, and which values the schema supplied
rather than the document.
The root¶
decode hands back the root element's content: code reading one kind of
document already knows which element that is. When a schema set declares
several, pass root=True to be told which one the document was:
from pathlib import Path
schemas = xsdkit.SchemaSet.from_file("report.xsd")
schemas.decode(Path("report.xml"), root=True)
# {'report': {'@id': 'r1', 'title': 'November orders', ...}}
The key follows the rule every other key does, with the set's global elements as the siblings: the local name, in Clark notation only when another global element shares it.
Invalid documents¶
decode raises, where validate does not:
schemas = xsdkit.SchemaSet.from_file("report.xsd")
bad = '<report xmlns="urn:example" id="r1"><title>t</title></report>'
schemas.decode(bad) # DocumentError
The asymmetry is deliberate. validate is asked whether a document is
valid, so an invalid one is an answer. decode is asked for data, and handing
back data from a document that does not fit its schema is exactly the mistake
this is meant to remove.
Take it anyway when you mean to:
lax takes what an invalid document says. Text that is not XML at all says
nothing, so it raises either way:
Mixed content¶
Character data in a mixed type decodes under "$", alongside the children.
The runs are concatenated: what was said, not where it sat between the
children. For the data-oriented schemas this library is built for that is a
non-question; for marked-up prose, decode is the wrong tool and the PSVI
event stream is the right one.