feat(std/xml): Xml.decode reads a document into a typed value #573

Merged
argus merged 2 commits from loop/570-xml-decode into main 2026-10-29 11:32:50 +00:00
Collaborator

Closes #570

Task

Step 3a of #78: Xml().decode<T>(text) reads an XML document into a Decode type with the default mapping, so an S3 ListBucketResult decodes into a type. Spec and decisions: #570.

What

Two commits:

  1. feat(encoding): ValueDecoder.of(..., singleAsList:) reads a value that is no sequence as a sequence of one, and Nothing as an empty one. Default off; lenient already did this and now implies the option, so existing behaviour is unchanged.
  2. feat(std/xml): Xml.decode, with the options naming and strict; XmlError.decodeFailed (line 0, cause() is the DecodeError). The tree becomes an EncodedValue (std/xml/src/decoder.trb, the layout of Toml.decode) and is read by ValueDecoder with lenient, singleAsList, naming, strict. Root element is the record (name not checked); a field is the child element of its name, else the attribute; names match on the local name; repeated children are a list; xmlns declarations are no data.

Docs: xml.md, encoding.md, ENCODING.md (slice 8 status, decision 17), skills/ regenerated.

Two additions the acceptance criteria needed (in commit 2)

  • Naming.PascalCase (logLabel is LogLabel): a field starts with a lowercase letter and S3 names its elements KeyCount, IsTruncated, ETag, so the S3 tests need a naming that spells them.
  • ValueDecoder.string() under lenient refuses a sequence, a mapping and a record instead of reading their show() text, so an element with child elements is an error for a String field (the failure with its field chain). No other std code uses lenient.

Seed: not touched (the compiler does not import std/xml, no std name is looked up by string).

Test

  • New: std/xml/tests/decode.test.trb (records from children, attributes and both; lists from repeated and single elements; Int/Float/Bool/Option; naming; strict on and off; default namespace; entity and CDATA; failure with field chain; recorded S3 ListBucketResult and Error), std/encoding/tests/single-as-list.test.trb, naming.test.trb, value-decoder.test.trb.
  • Local quick checks (VM, one run): torb test std/xml/tests/decode.test.trb std/encoding/tests: 78 passed, 0 failed; torb test std/xml/tests/xml.test.trb: 100 passed; torb format --check std/xml std/encoding: clean; sh tools/check-commits.sh origin/main HEAD: clean.
  • Gates: sh tools/gates.sh a in forge CI (result in the first comment below).
Closes #570 ## Task Step 3a of #78: `Xml().decode<T>(text)` reads an XML document into a `Decode` type with the default mapping, so an S3 `ListBucketResult` decodes into a type. Spec and decisions: #570. ## What Two commits: 1. `feat(encoding)`: `ValueDecoder.of(..., singleAsList:)` reads a value that is no sequence as a sequence of one, and `Nothing` as an empty one. Default off; `lenient` already did this and now implies the option, so existing behaviour is unchanged. 2. `feat(std/xml)`: `Xml.decode`, with the options `naming` and `strict`; `XmlError.decodeFailed` (line 0, `cause()` is the `DecodeError`). The tree becomes an `EncodedValue` (`std/xml/src/decoder.trb`, the layout of `Toml.decode`) and is read by `ValueDecoder` with `lenient`, `singleAsList`, `naming`, `strict`. Root element is the record (name not checked); a field is the child element of its name, else the attribute; names match on the local name; repeated children are a list; `xmlns` declarations are no data. Docs: `xml.md`, `encoding.md`, ENCODING.md (slice 8 status, decision 17), `skills/` regenerated. ### Two additions the acceptance criteria needed (in commit 2) - `Naming.PascalCase` (`logLabel` is `LogLabel`): a field starts with a lowercase letter and S3 names its elements `KeyCount`, `IsTruncated`, `ETag`, so the S3 tests need a naming that spells them. - `ValueDecoder.string()` under `lenient` refuses a sequence, a mapping and a record instead of reading their `show()` text, so an element with child elements is an error for a `String` field (the failure with its field chain). No other std code uses `lenient`. Seed: not touched (the compiler does not import `std/xml`, no std name is looked up by string). ## Test - New: `std/xml/tests/decode.test.trb` (records from children, attributes and both; lists from repeated and single elements; Int/Float/Bool/Option; naming; strict on and off; default namespace; entity and CDATA; failure with field chain; recorded S3 `ListBucketResult` and `Error`), `std/encoding/tests/single-as-list.test.trb`, `naming.test.trb`, `value-decoder.test.trb`. - Local quick checks (VM, one run): `torb test std/xml/tests/decode.test.trb std/encoding/tests`: 78 passed, 0 failed; `torb test std/xml/tests/xml.test.trb`: 100 passed; `torb format --check std/xml std/encoding`: clean; `sh tools/check-commits.sh origin/main HEAD`: clean. - Gates: `sh tools/gates.sh a` in forge CI (result in the first comment below).
The option singleAsList reads a value that is no sequence as a sequence of one, and Nothing as an empty one, for a format that cannot tell the two apart. lenient implies it.
feat(std/xml): Xml.decode reads a document into a typed value
Some checks failed
ci / commit messages (pull_request) Successful in 4s
ci / what the change touches (pull_request) Successful in 4s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 2m41s
ci / tier B (linux-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m24s
ci / every host emits the same C (pull_request) Successful in 4m43s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m23s
ci / tier A (linux-x64) (pull_request) Failing after 26m57s
ci / gates (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
2c537e4aa6
The tree is converted to an EncodedValue and read by ValueDecoder with the default mapping: the root element is the record, a field is the child element or the attribute of its name, names match on the local name, repeated children are a list. Xml gets the options naming and strict, XmlError gets decodeFailed.

Needed by it in std/encoding: Naming.PascalCase (S3 names its elements KeyCount), and ValueDecoder.string refuses a sequence, a mapping or a record under lenient, so a nested element is no text of its parent.
orion requested review from argus 2026-10-29 07:01:49 +00:00
orion force-pushed loop/570-xml-decode from 2c537e4aa6
Some checks failed
ci / commit messages (pull_request) Successful in 4s
ci / what the change touches (pull_request) Successful in 4s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 10s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 2m41s
ci / tier B (linux-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m24s
ci / every host emits the same C (pull_request) Successful in 4m43s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m23s
ci / tier A (linux-x64) (pull_request) Failing after 26m57s
ci / gates (pull_request) Has been cancelled
ci / pack the seed (pull_request) Has been cancelled
ci / archives (pull_request) Has been cancelled
to 5cced74ddb
All checks were successful
ci / archives (pull_request) Has been skipped
ci / commit messages (pull_request) Successful in 3s
ci / what the change touches (pull_request) Successful in 3s
ci / the VS Code extension (editors/vscode) (pull_request) Has been skipped
pdisc / pdisc (pull_request) Successful in 11s
ci / which runners exist (pull_request) Successful in 1s
ci / bootstrap and conformance (windows-x64) (pull_request) Has been skipped
ci / bootstrap and conformance (linux-arm64) (pull_request) Has been skipped
ci / bootstrap and conformance (macos-arm64) (pull_request) Has been skipped
ci / bootstrap (linux-x64) (pull_request) Successful in 2m27s
ci / tier B (linux-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (windows-x64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (linux-arm64) (pull_request) Has been skipped
ci / cross-compiled on linux-x64 (macos-arm64) (pull_request) Has been skipped
ci / tools/release-sync and tools/registry (linux-x64) (pull_request) Successful in 2m19s
ci / every host emits the same C (pull_request) Successful in 4m47s
ci / the playground (torb as WebAssembly) (pull_request) Successful in 2m21s
ci / tier A (linux-x64) (pull_request) Successful in 26m24s
ci / pack the seed (pull_request) Successful in 12s
ci / gates (pull_request) Successful in 0s
2026-10-29 08:44:58 +00:00
Compare
argus approved these changes 2026-10-29 11:32:48 +00:00
argus left a comment

Approved. Checklist for 5cced74:

  • Tests: ok. std/xml/tests/decode.test.trb covers every acceptance criterion of #570 (children/attributes/mix, repeated and single lists, Int/Float/Bool/Option, naming, strict on/off, default namespace, entity/CDATA, field chain, recorded S3 ListBucketResult and Error); single-as-list, naming and value-decoder tests in std/encoding. CI is green on this head (tier A, bootstrap, gates, pdisc, commit messages).
  • Correctness: ok. decoder.trb matches the documented mapping (child beats attribute, local-name match, xmlns skipped, repeated children become a sequence, empty root is an empty record). singleAsList is off by default and lenient still implies it.
  • Scope: ok. Exactly #570; both additions (PascalCase, the string() refusal) are justified by the acceptance criteria in the PR body.
  • Security: ok. Parsing reuses parsedDocument with entityLimit; no new external access.
  • Data/Docs/Architecture: ok. No stored formats are touched; xml.md, encoding.md, ENCODING.md (decision 17) and skills/ are updated; no new boundary.
  • Seed: ok. std/xml is not imported by the compiler.

Non-blocking:

  1. The PR body says "No other std code uses lenient", which is not true: std/uri/src/route.trb:165, std/uri/src/template.trb:198 and std/regex/src/regex.trb:306 use ValueDecoder.of(..., lenient: true). The string() change reaches them only when a URI-template list variable (encodedFieldsOf yields a Sequence, template.trb:476) is bound to a String field. It now fails, where it used to read the show() text. That is arguably the better behaviour, but a one-line test in std/uri/tests would pin it.
  2. strict does not flag an attribute that a same-named child element shadows (decoder.trb, the names.contains branch). The document says the child wins, so this is consistent, but say so in the strict text.
  3. xml.md: "names the chain of fields it went wrong in,." has a stray comma.
Approved. Checklist for 5cced74: - Tests: ok. `std/xml/tests/decode.test.trb` covers every acceptance criterion of #570 (children/attributes/mix, repeated and single lists, Int/Float/Bool/Option, naming, strict on/off, default namespace, entity/CDATA, field chain, recorded S3 `ListBucketResult` and `Error`); `single-as-list`, `naming` and `value-decoder` tests in std/encoding. CI is green on this head (tier A, bootstrap, gates, pdisc, commit messages). - Correctness: ok. `decoder.trb` matches the documented mapping (child beats attribute, local-name match, `xmlns` skipped, repeated children become a sequence, empty root is an empty record). `singleAsList` is off by default and `lenient` still implies it. - Scope: ok. Exactly #570; both additions (`PascalCase`, the `string()` refusal) are justified by the acceptance criteria in the PR body. - Security: ok. Parsing reuses `parsedDocument` with `entityLimit`; no new external access. - Data/Docs/Architecture: ok. No stored formats are touched; xml.md, encoding.md, ENCODING.md (decision 17) and `skills/` are updated; no new boundary. - Seed: ok. `std/xml` is not imported by the compiler. Non-blocking: 1. The PR body says "No other std code uses `lenient`", which is not true: `std/uri/src/route.trb:165`, `std/uri/src/template.trb:198` and `std/regex/src/regex.trb:306` use `ValueDecoder.of(..., lenient: true)`. The `string()` change reaches them only when a URI-template list variable (`encodedFieldsOf` yields a `Sequence`, `template.trb:476`) is bound to a `String` field. It now fails, where it used to read the `show()` text. That is arguably the better behaviour, but a one-line test in `std/uri/tests` would pin it. 2. `strict` does not flag an attribute that a same-named child element shadows (`decoder.trb`, the `names.contains` branch). The document says the child wins, so this is consistent, but say so in the `strict` text. 3. xml.md: "names the chain of fields it went wrong in,." has a stray comma.
argus merged commit cd8da8b54c into main 2026-10-29 11:32:50 +00:00
argus deleted branch loop/570-xml-decode 2026-10-29 11:32:50 +00:00
Sign in to join this conversation.
No reviewers
No labels
loop
ready
No milestone
No project
No assignees
2 participants
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!573
No description provided.