formats: Xml.decode reads a document into a typed value (step 3a of #78) #570

Closed
opened 2026-10-29 06:36:27 +00:00 by orion · 6 comments
Collaborator

Context

Step 3a of the epic #78 (std/xml). Steps 1 (#429, tree, reader, writer) and 2 (#552, internal subset) have landed. S3 listings and errors (#20) and the WebDAV multistatus replies (#67) need typed reading first; writing follows as step 3b once this has landed.

Goal

Xml().decode<T>(text) reads an XML document into a Decode type with the default mapping, so an S3 ListBucketResult decodes into a type.

Source

  • ENCODING.md section 9 (document formats: the tree for documents, binding for data that travels as XML), slice 8 (section 14), the lenient decoder of ValueDecoder (section 4 vocabulary), and decision 15 in section 16.
  • std/toml/src/decoder.trb and Toml.decode are the layout model (tree to EncodedValue, then ValueDecoder).

Design record point

ENCODING.md slice 8, section 9. Touches the seed? No: the compiler does not import std/xml, and no std name is looked up by string.

Decisions (decided by the loop)

  • The tree is read through the existing parse, then converted to an EncodedValue and read by ValueDecoder.of(..., lenient: true, naming:, strict:); XML has only text, so int() and float() parse it.
  • The root element is the record. A field is the child element of its name; if no such child exists, an attribute of that name is read (the "attribute read back from a child element and the reverse" of the epic). Names match on the local name; the namespace is ignored, which S3 (xmlns="http://s3.amazonaws.com/doc/2006-03-01/") needs. The root element's own name is not checked.
  • A field of a scalar type reads the text of its element (CDATA and entity text included, comments and instructions skipped); an element that has child elements and no matching type fails with the chain of fields, like Toml.decode.
  • Repeated children of one name read as a list. A single child where a list is expected also reads as a one-item list, because XML cannot tell the two apart. ValueDecoder has no such option yet, so commit 1 adds one (singleAsList, default off, documented in std/encoding); commit 2 is the decoder.
  • Options on Xml: naming (as Toml.naming) and strict (an unread child element or attribute is an error). decode answers Result<Value, XmlError>; XmlError gets a decodeFailed constructor like TomlError.decodeFailed.
  • Format<XmlError> and Xml.encode are step 3b, not here.

Acceptance criteria

  • Xml().decode<T>(text) exists with the documented behaviour; a missing field takes its default or None, a text that is not well-formed is the XmlError of parse, a type mismatch is an XmlError with the field chain.
  • Tests in std/xml/tests: a record from child elements, from attributes, and from a mix; a List<T> from repeated elements and from a single one; Int, Float, Bool and Option fields from text; naming; strict on and off; a default-namespace document; an entity and a CDATA value; a failure with its field chain.
  • A test decodes a recorded S3 ListBucketResult (Name, Prefix, IsTruncated, Contents with Key, Size, LastModified, ETag) and an S3 Error document (Code, Message, RequestId) into types.
  • A test for singleAsList in std/encoding (on and off).
  • docs/standard-library/xml.md and docs/standard-library/encoding.md document the new surface, skills/ is regenerated, and ENCODING.md section 14 and 16 record the step (new decision point).
  • Gates sh tools/gates.sh a green in forge CI; tools/check-commits.sh clean.

Size

One PR, two Conventional Commits: feat(encoding): ValueDecoder reads a single value as a one-item list on request and feat(std/xml): Xml.decode reads a document into a typed value.

Dependencies

Part of #78 (step 3a). Unblocks step 3b (encode, Format<XmlError>), then #20 and #67 (typed replies).

## Context Step 3a of the epic #78 (`std/xml`). Steps 1 (#429, tree, reader, writer) and 2 (#552, internal subset) have landed. S3 listings and errors (#20) and the WebDAV multistatus replies (#67) need typed reading first; writing follows as step 3b once this has landed. ## Goal `Xml().decode<T>(text)` reads an XML document into a `Decode` type with the default mapping, so an S3 `ListBucketResult` decodes into a type. ## Source - ENCODING.md section 9 (document formats: the tree for documents, binding for data that travels as XML), slice 8 (section 14), the lenient decoder of `ValueDecoder` (section 4 vocabulary), and decision 15 in section 16. - `std/toml/src/decoder.trb` and `Toml.decode` are the layout model (tree to `EncodedValue`, then `ValueDecoder`). ## Design record point ENCODING.md slice 8, section 9. Touches the seed? No: the compiler does not import `std/xml`, and no std name is looked up by string. ## Decisions (decided by the loop) - The tree is read through the existing `parse`, then converted to an `EncodedValue` and read by `ValueDecoder.of(..., lenient: true, naming:, strict:)`; XML has only text, so `int()` and `float()` parse it. - The root element is the record. A field is the child element of its name; if no such child exists, an attribute of that name is read (the "attribute read back from a child element and the reverse" of the epic). Names match on the local name; the namespace is ignored, which S3 (`xmlns="http://s3.amazonaws.com/doc/2006-03-01/"`) needs. The root element's own name is not checked. - A field of a scalar type reads the text of its element (CDATA and entity text included, comments and instructions skipped); an element that has child elements and no matching type fails with the chain of fields, like `Toml.decode`. - Repeated children of one name read as a list. A single child where a list is expected also reads as a one-item list, because XML cannot tell the two apart. `ValueDecoder` has no such option yet, so commit 1 adds one (`singleAsList`, default off, documented in `std/encoding`); commit 2 is the decoder. - Options on `Xml`: `naming` (as `Toml.naming`) and `strict` (an unread child element or attribute is an error). `decode` answers `Result<Value, XmlError>`; `XmlError` gets a `decodeFailed` constructor like `TomlError.decodeFailed`. - `Format<XmlError>` and `Xml.encode` are step 3b, not here. ## Acceptance criteria - `Xml().decode<T>(text)` exists with the documented behaviour; a missing field takes its default or `None`, a text that is not well-formed is the `XmlError` of `parse`, a type mismatch is an `XmlError` with the field chain. - Tests in `std/xml/tests`: a record from child elements, from attributes, and from a mix; a `List<T>` from repeated elements and from a single one; `Int`, `Float`, `Bool` and `Option` fields from text; `naming`; `strict` on and off; a default-namespace document; an entity and a CDATA value; a failure with its field chain. - A test decodes a recorded S3 `ListBucketResult` (`Name`, `Prefix`, `IsTruncated`, `Contents` with `Key`, `Size`, `LastModified`, `ETag`) and an S3 `Error` document (`Code`, `Message`, `RequestId`) into types. - A test for `singleAsList` in `std/encoding` (on and off). - `docs/standard-library/xml.md` and `docs/standard-library/encoding.md` document the new surface, `skills/` is regenerated, and ENCODING.md section 14 and 16 record the step (new decision point). - Gates `sh tools/gates.sh a` green in forge CI; `tools/check-commits.sh` clean. ## Size One PR, two Conventional Commits: `feat(encoding): ValueDecoder reads a single value as a one-item list on request` and `feat(std/xml): Xml.decode reads a document into a typed value`. ## Dependencies Part of #78 (step 3a). Unblocks step 3b (encode, `Format<XmlError>`), then #20 and #67 (typed replies).
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T12:46Z

[loop:torbscript] claim implement by session:loop-torbscript-20261029 until 2026-10-29T12:46Z
Author
Collaborator

[loop:torbscript] checkpoint: done: commit 1 (singleAsList option, test, docs, skills) on branch loop/570-xml-decode; in flight: commit 2 (Xml.decode); next: tests, docs, ENCODING.md, push, PR. Finding: ValueDecoder already reads a single value as a one-item list under lenient, so the new option makes it independent of lenient (lenient implies it).

[loop:torbscript] checkpoint: done: commit 1 (singleAsList option, test, docs, skills) on branch loop/570-xml-decode; in flight: commit 2 (Xml.decode); next: tests, docs, ENCODING.md, push, PR. Finding: ValueDecoder already reads a single value as a one-item list under lenient, so the new option makes it independent of lenient (lenient implies it).
Author
Collaborator

[loop:torbscript] checkpoint: PR #573 open (branch loop/570-xml-decode, two commits, review requested from argus); CI pending (gates, commit messages, touches, pdisc); next: read CI result, fix red on the same branch, then write-back.

[loop:torbscript] checkpoint: PR https://git.torb.dev/torbscript/language/pulls/573 open (branch loop/570-xml-decode, two commits, review requested from argus); CI pending (gates, commit messages, touches, pdisc); next: read CI result, fix red on the same branch, then write-back.
Author
Collaborator

[loop:torbscript] implement: #573, CI pending (commit messages, touches, pdisc and bootstrap green; tier A / gates still running after ~50 min), worker usage ~400k tokens, next: waiting (CI result, then merge gate: argus review). Notes: ValueDecoder already read a lone value as a list under lenient, so singleAsList is the independent option with lenient implying it; the S3 tests needed Naming.PascalCase and a lenient String refusing records, both in commit 2 and listed in the PR body.

[loop:torbscript] implement: https://git.torb.dev/torbscript/language/pulls/573, CI pending (commit messages, touches, pdisc and bootstrap green; tier A / gates still running after ~50 min), worker usage ~400k tokens, next: waiting (CI result, then merge gate: argus review). Notes: ValueDecoder already read a lone value as a list under lenient, so singleAsList is the independent option with lenient implying it; the S3 tests needed Naming.PascalCase and a lenient String refusing records, both in commit 2 and listed in the PR body.
Author
Collaborator

[loop:torbscript] claim fix by session:loop-torbscript-20261029 until 2026-10-29T14:45Z

[loop:torbscript] claim fix by session:loop-torbscript-20261029 until 2026-10-29T14:45Z
Author
Collaborator

[loop:torbscript] review: the red tier A (linux-x64) lane was caused by this change, not flaky: docs check and docs index --check failed because the std/xml summary had grown to 278 characters (limit 240) and the generated indexes were stale; every other tier A gate was green. Fixed on the same branch by shortening the summary to 238 characters and refreshing the six copies of it (docs and generated skills), folded into the Xml.decode commit (rebased onto the same base, force-with-lease; head now 5cced74d, tools/check-commits.sh clean). Forge CI for the new head is queued behind other runs on the runner pool (run 2257 waiting), next: waiting (forge CI and merge gate). Worker usage: ~0.4M tokens.

[loop:torbscript] review: the red `tier A (linux-x64)` lane was caused by this change, not flaky: `docs check` and `docs index --check` failed because the `std/xml` summary had grown to 278 characters (limit 240) and the generated indexes were stale; every other tier A gate was green. Fixed on the same branch by shortening the summary to 238 characters and refreshing the six copies of it (docs and generated skills), folded into the Xml.decode commit (rebased onto the same base, force-with-lease; head now 5cced74d, `tools/check-commits.sh` clean). Forge CI for the new head is queued behind other runs on the runner pool (run 2257 waiting), next: waiting (forge CI and merge gate). Worker usage: ~0.4M tokens.
argus closed this issue 2026-10-29 11:32:50 +00:00
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#570
No description provided.