PureXML/README.md
Mihaela Mihaljevic 1fbe359ada feat(validation): composable validation framework + structural rule
Slice 1 of #92. Introduce the OpenAPIKit-idiom validation framework, replacing
the imperative if-tree structural validator:

- ValidationError (reason + coding path) renders "<reason> at path: root/row/cell";
  ValidationErrorCollection gathers every failure and is thrown once.
- Validation<Subject, Document> is the atomic, composable rule value, with the
  multi-error and single-error Bool inits (the latter auto-forms "Failed to
  satisfy: <description>"). Predicate-gated, type-directed via an erased
  AnyValidation that guards an optional from matching a non-optional rule.
- The framework is parameterized over both Subject and Document so the structural,
  DTD, XSD, and Schematron layers share one machinery (Document = Void for rules
  that need only their subject).
- Validator<Document> walks the node tree, building the coding path, with blank /
  default factories and fluent validating / withoutValidating.
- The combinator algebra (&&, ||, comparison lifts, take, lift, unwrap, all) as
  free functions returning closures.

The structural duplicate-attribute rule is reborn as Validation<Element, Void>.
Combinators and the erased wrapper are unit-tested as pure functions per the rule.

The legacy Validation.Issue stays only for the not-yet-migrated schema validators
and is retired in a later slice.
2026-06-08 12:00:34 +02:00

206 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PureXML
[![Style and namespacing](https://github.com/mihaelamj/PureXML/actions/workflows/style.yml/badge.svg)](https://github.com/mihaelamj/PureXML/actions/workflows/style.yml)
[![Swift macOS](https://github.com/mihaelamj/PureXML/actions/workflows/swift-macos.yml/badge.svg)](https://github.com/mihaelamj/PureXML/actions/workflows/swift-macos.yml)
[![Swift Linux](https://github.com/mihaelamj/PureXML/actions/workflows/swift-linux.yml/badge.svg)](https://github.com/mihaelamj/PureXML/actions/workflows/swift-linux.yml)
[![Swift Windows](https://github.com/mihaelamj/PureXML/actions/workflows/swift-windows.yml/badge.svg)](https://github.com/mihaelamj/PureXML/actions/workflows/swift-windows.yml)
[![Swift WASM](https://github.com/mihaelamj/PureXML/actions/workflows/swift-wasm.yml/badge.svg)](https://github.com/mihaelamj/PureXML/actions/workflows/swift-wasm.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
PureXML is a dependency-free XML package written entirely in Swift.
The goal is a Linux-, Windows-, and WebAssembly-compatible XML reader/writer that
does not pull in `libxml2`, `expat`, or Foundation's `XMLParser`. The package is
intentionally strict about portability:
- no external SwiftPM dependencies
- no bundled C sources
- no Foundation requirement in the library target
- root Swift package layout
- macOS, Linux, Windows, and WASI build gates
It is a sibling project to [PureYAML](https://github.com/mihaelamj/PureYAML) and
follows the same structure, rules, and verification gates.
## Roadmap
The roadmap uses the TileDown Mermaid palette: green for shipped work, yellow for
review, purple for epic grouping, and gray for open work.
```mermaid
flowchart TB
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
LDone["In main now"]:::done
LReview["PR in review"]:::review
LEpic["Epic grouping"]:::epic
LTodo["Open issue, no PR"]:::todo
Core["Core shipped: #1 push, #19 SAX, #28 encoding, #29 entities, #31 tree, #32 writer, #33 reader, #34 save, #35 chars"]:::done
Query["Query + validation shipped: #21 #22 #23 query, #30 regex, #2 XSD/RELAX NG, #25 Schematron"]:::done
Transform["Transform + HTML shipped: #3 XSLT, #20 HTML, #24 XInclude, #26 C14N, #27 Catalog"]:::done
Parity["Parity completion shipped: #61 (XSLT keys/output, XSD union/import/identity, RELAX NG compact, HTML5 doc structure, C14N 2.0 trim)"]:::done
LDone --> LReview
LReview --> LEpic
LEpic --> LTodo
```
The goal is full libxml2 feature parity in pure Swift with zero dependencies, by
clean-room reimplementation (behavior reproduced from the private PureXML-research
analysis, no upstream source copied). The streaming core plus DTD validation
(content models, attributes, ID/IDREF) and an XPath 1.0 subset is shipped; the
remaining libxml2 surface is tracked as epics. Deliberate non-goals: network
fetching (`nanohttp`/`nanoftp`) and the threading/memory infrastructure stay out,
and external resolution is opt-in through an injected resolver so XXE stays closed.
The original epics and the documented-subset parity work (#61) are all shipped
(summarized above). The current focus is the differentiator: fault-tolerant
reading and a rich, composable validation layer (#92). A read-only conformance
audit also surfaced the remaining depth gaps versus libxml2 (#71):
```mermaid
flowchart TB
classDef done fill:#ddf9e4,stroke:#34c759,color:#111827
classDef review fill:#fff7d6,stroke:#ffcc00,color:#111827
classDef epic fill:#f2e5ff,stroke:#af52de,color:#111827
classDef todo fill:#f2f4f7,stroke:#8e8e93,color:#111827
Validation["#92 OpenAPIKit-idiom validation framework + recovering reader: read invalid documents without crashing, return rich located results"]:::epic
Gaps["#71 Beyond-parity libxml2 gaps: DTD entities/defaulting (#72 #73), parser breadth (#74), XSD instance/derivation/wildcards/namespaces (#75 #76 #77 #78), RELAX NG + Schematron (#79 #80), XSLT elements/functions (#81 #82), HTML5 full tree (#83 #84), C14N/catalog/XInclude (#85 #86 #87), XPath/XPointer (#88), regex (#89), DOM/serialization (#90 #91)"]:::epic
Validation --> Gaps
```
## Status
PureXML is a working, dependency-free XML library today: parse, emit, validate,
query, and stream documents on macOS, Linux, Windows, and WASM. The test suite
currently runs **135 tests in 18 suites** (`swift test`).
### Shipped (libxml2-aligned surface)
- **Model** (`PureXML.Model`): `Node`, `Element`, `Attribute`, `QualifiedName`,
and a mutable `TreeNode` for in-place editing (insert, remove, replace, copy,
parent navigation).
- **Parsing** (`PureXML.Parsing`): iterative streaming parser over strings,
bytes, or incremental character/byte sources. Pull events via `EventReader`,
SAX2-style callbacks via `SAXHandler`, and a pull cursor via `TextReader`
(libxml2 `xmlTextReader`). Handles elements, attributes, text, predefined and
numeric entities, comments, CDATA, and processing instructions. Namespace
bindings (`xmlns`) are resolved to URIs on parse.
- **Encoding**: UTF-8/16/32 (with BOM sniffing), ISO-8859-1, and Windows-1252
from raw bytes or streaming byte input.
- **DTD (opt-in)**: `<!DOCTYPE>` is **off by default** (`Limits.allowDoctype:
false`) to keep XXE and entity-expansion closed. When enabled, the internal
subset is parsed; internal general entities expand with a bounded cap; external
entities and external subsets are refused unless an injected
`EntityResolver` supplies replacement text.
- **Validation** (`PureXML.Validation`): structural checks (duplicate attributes,
etc.) plus **DTD validation** against declared content models, attribute lists,
and ID/IDREF rules (`DTDSchema`, `validateAgainstInternalDTD`). XSD, RELAX NG,
and Schematron remain open epics (#2, #25).
- **Emitting** (`PureXML.Emitting`): `Serializer` (save-option parity) and an
incremental `Writer` (libxml2 `xmlTextWriter`) with compact and pretty modes.
- **XPath** (`PureXML.XPath`): a **subset** of XPath 1.0 (location paths, common
axes, node tests, basic predicates). Full XPath 1.0 is epic #21.
### Safe defaults
By default the parser rejects `<!DOCTYPE>` and refuses every external reference
(`EntityResolver.refusing`). Enable DTD processing only when you need internal
subset validation or internal entities, and wire a resolver only for trusted,
in-memory replacements. Network fetching (`nanohttp`/`nanoftp`) and libxml2's
threading/memory infrastructure are deliberate non-goals.
### Remaining toward full libxml2 feature parity
**12 of 20 epics are still open**; each now has filed child issues (#4–#60). The
shipped eight cover the core I/O stack; most remaining work is whole standards:
| Open epic | Child issues | What it adds |
|---|---|---|
| #1 Push / feed API | #4–#7 | Resumable scanner + push parser |
| #20 HTML parser | #36–#39 | Tag-soup HTML parse and serialize |
| #21 Full XPath 1.0 | #40–#43 | Unlocks #22, #23, #25, #3 |
| #22 xmlPattern | #44–#45 | Streaming pattern match (needs #21) |
| #23 XPointer | #46–#47 | element() / xpointer() (needs #21) |
| #24 XInclude | #48–#51 | URI, xml:base, xi:include |
| #25 Schematron | #52–#53 | Rule validation (needs #21) |
| #26 C14N | #54–#56 | Canonical XML variants |
| #27 XML Catalog | #57–#58 | OASIS catalog resolver |
| #30 → #2 | #59–#60, #8–#13 | Regex engine, then XSD / RELAX NG |
| #3 XSLT 1.0 | #14–#18 | Stylesheet transform (needs #21) |
**40 open enhancement issues** under those epics (plus the 12 epic parents). Critical
path: **#30 → #2** (schema) and **#21 → #22/#23/#25/#3** (query/transform).
## Usage
```swift
import PureXML
// Build a tree and emit it (works today).
let element = PureXML.Model.Element(
"book",
attributes: [.init("id", "bk101")],
children: [.element(.init("title", children: [.text("XML Developer's Guide")]))],
)
let xml = PureXML.serialize(.element(element))
try PureXML.validate(.element(element))
// Parse it back into a tree.
let node = try PureXML.parse(xml)
// Or stream events without building a tree (drives chunked input too).
var reader = PureXML.events(xml)
while let event = try reader.next() {
// handle .startElement / .characters / .endElement / ...
}
// DTD validation (opt-in: allowDoctype + optional resolver for trusted externals).
let issues = try PureXML.validateAgainstInternalDTD(
xmlWithDoctype,
limits: .init(allowDoctype: true),
)
// XPath subset over a parsed tree.
let titles = try PureXML.xpath("//title", over: node)
.compactMap(\.element)
```
## Attribution
PureXML is informed by the behavior of established XML parsers (`libxml2`,
`expat`, Foundation's `XMLParser`) and by the W3C XML 1.0 specification, but it
does not copy their implementation into `Sources/`. See
[ATTRIBUTION.md](ATTRIBUTION.md).
## Development Contract
PureXML must stay dependency-free and portable. Before merging changes:
- Swift tools version: 6.1
- Package products: `PureXML`
- SwiftPM dependencies: none
- Hosted CI matrix: macOS, Linux, Windows, and WASM
```sh
bash scripts/check-all.sh
```
That command expands to:
```sh
bash scripts/check-style.sh
bash scripts/check-namespacing.sh
bash scripts/check-forbidden-patterns.sh
swiftformat . --config .swiftformat --lint
swiftlint --config .swiftlint.yml --strict
swift build
swift test
```
## License
MIT.