11 KiB
Migration and YAML Support Boundaries
PureYAML can replace a C-backed YAML parser when the caller needs dependency-free Swift parsing, emitting, validation, and typed conversion for the currently tested YAML subset.
It should not be treated as a drop-in full YAML implementation yet. The current contract is the behavior pinned by the Swift Testing corpus.
Attribution Boundary
PureYAML is informed by the behavior and public API shape of Yams and its bundled
CYaml / libyaml-derived parser. The public implementation is original Swift
code in this repository. Do not copy Yams, CYaml, or libyaml implementation code
into Sources/.
See ../ATTRIBUTION.md for the project attribution.
Migration Map
| Existing need | PureYAML path | Notes |
|---|---|---|
| Compose YAML into a native YAML tree | PureYAML.parse(_:) |
Returns PureYAML.Model.Value, preserving mapping order and duplicate keys. |
| Compose all YAML documents in a stream | PureYAML.parseStream(_:) |
Returns indexed PureYAML.Stream.Document values. |
| Preserve explicit YAML tags for analysis | PureYAML.parseTagged(_:) or PureYAML.parseTaggedStream(_:) |
Returns tag-preserving nodes without Foundation-backed construction. |
| Serialize a YAML tree | PureYAML.dump(_:options:) |
Emits deterministic YAML with explicit block, flow, and scalar policies. |
| Serialize all YAML documents in a stream | PureYAML.dump(_:options:) with [PureYAML.Stream.Document] |
Emits deterministic explicit document starts and preserves array order. |
Decode a Decodable type from YAML |
PureYAML.decode(_:from:) |
Runs default validation before typed decoding. |
Encode an Encodable value to YAML |
PureYAML.encodeToYAML(_:options:) |
Encodes through PureYAML.Model.Value, then dumps. |
| Validate document structure | PureYAML.validate(_:using:strict:) |
Default rule reports duplicate mapping keys with exact paths. |
| Validate every document in a stream | PureYAML.validate(_:using:strict:) with stream documents |
Reports PureYAML.Stream.Issue values that preserve document indexes. |
| Validate explicit tag usage | PureYAML.Tagged.Validator |
Reports unsupported built-in tags and kind-inconsistent built-in tags with exact paths. |
| Construct caller-owned values from explicit tags | PureYAML.Tagged.Constructor |
Empty by default; callers register exact tag/kind handlers or an explicit fallback. |
Load arbitrary Any dictionaries and arrays |
Use Model.Value or typed Codable |
There is deliberately no public arbitrary Any conversion API. Dictionary-shaped values erase YAML diagnostics. |
Arbitrary Any Decision
PureYAML does not expose loadAny, dumpAny, or dictionary-and-array Any
entry points. That is a deliberate compatibility boundary, not a missing
wrapper.
The research implementation's arbitrary-value constructor is useful to study, but its observable shape conflicts with PureYAML's first principles:
- mapping conversion becomes dictionary-shaped, so duplicate keys and insertion order are not a first-class contract;
- merge-key behavior can be hidden inside construction rather than exposed as an explicit parser contract;
- scalar construction includes Foundation-specific values such as binary data, dates, and null objects;
- custom constructors can hide whether a document was parsed, normalized, flattened, or validated.
For unknown schemas, use PureYAML.Model.Value and validate before projecting
into project-specific values. For known schemas, use typed Codable. If a
caller truly needs a JSON-like dictionary after validation, build that projection
in the application layer where duplicate-key and unsupported-shape policies are
explicit. If a caller needs custom tag construction, use
PureYAML.Tagged.Constructor so unsupported tags, wrong-kind tags, and
tag-erasing fallbacks stay explicit.
Supported Today
The supported list below is covered by tests. When adding a new migration case, add a fixture beside the closest existing suite.
| Area | Current behavior | Test coverage |
|---|---|---|
| Block mappings and sequences | Parsed into ordered mappings and sequences. | ParsingTests, CollectionCompatibilityFixtures |
| Common scalars | Null, booleans, integers, doubles, and strings resolve into model values. | ParsingTests, ScalarCompatibilityFixtures |
| Quoted strings | Single and double quoted strings keep string intent. | ParsingTests, ScalarCompatibilityFixtures |
| Comments | Comments outside quoted strings are ignored. Hash characters inside quoted strings remain content. | ParsingTests |
| Flow collections | Flow sequences and flow mappings compose into model values. | ParsingTests, CollectionCompatibilityFixtures |
| Literal and folded blocks | Block scalar text is preserved according to current parser rules. | ParsingTests, DumperTests |
| Anchors and aliases | Scalar and mapping anchors can be reused through aliases. Undefined aliases throw exact parse errors. | ParsingTests, CollectionCompatibilityFixtures |
| Merge keys | Plain << and explicit !!merge keys expand mapping values and sequence-of-mapping values. Local keys override inherited keys; duplicate local keys remain visible to validation. |
MergeKeyCompatibilityTests, ParsingCompatibilityTests, DownstreamDocumentTests |
| YAML directives and document markers | %YAML 1.2, selected %TAG expansion, ---, and ... are supported. |
ParsingCompatibilityTests, StreamParsingTests |
| Multi-document streams | PureYAML.parseStream(_:) parses implicit and explicit documents, empty explicit documents as null, ... document ends, and trailing comments before the next ---. |
StreamParsingTests |
| Multi-document stream dumping | PureYAML.dump(_:) with stream documents emits explicit --- before every document, preserves array order, and round-trips through parseStream(_:). |
StreamDumpingTests |
| Stream validation | Stream validation preserves document indexes while keeping validation paths document-local. | StreamParsingTests |
| Built-in scalar tags | !!str, !!int, !!float, !!bool, and !!null are applied when valid. |
ScalarCompatibilityFixtures |
| Tag-preserving parsing | parseTagged(_:) and parseTaggedStream(_:) preserve explicit scalar and collection tags for compatibility analysis. |
TaggedCompatibilityTests |
| Tagged validation | Unsupported built-in tags and built-in tags applied to the wrong node kind are reported with exact paths. Project-specific tags are allowed by default. | TaggedCompatibilityTests |
| Tagged construction | PureYAML.Tagged.Constructor supports scalar, sequence, and mapping handlers, exact missing-constructor errors, exact kind-mismatch errors, recursive path-aware construction, and an explicit model-value fallback that erases tags without collapsing duplicate keys. |
TaggedConstructorTests |
| Complex mapping keys | Sequence and mapping keys parse into Model.Pair.keyNode, validate with exact paths, and dump as explicit YAML keys. String-keyed APIs remain string-keyed. |
ComplexMappingKeyParsingTests, ComplexMappingKeyValidationTests, ComplexMappingKeyDumperTests, ComplexMappingKeyCodableTests |
| Emitting | Deterministic block output by default, with opt-in flow collections and conservative literal blocks. | DumperTests, EmitterCorpusTests |
| Typed conversion | Scalar, keyed, unkeyed, nested, sequence, dynamic-key, and super-coder cases are covered. | CodingTests, CodableCompatibilityTests |
| Validation | Duplicate mapping keys are reported with exact issue paths. | ValidationTests, ValidationModelTests |
| Downstream-shaped documents | API-style and service-configuration documents parse and decode through representative paths. | DownstreamDocumentTests |
Current Unsupported Behavior
Unsupported behavior is either rejected with an exact error or preserved as an explicit fallback value tree. This avoids silent compatibility drift.
| YAML pattern | Current PureYAML behavior | Test coverage |
|---|---|---|
Passing a multi-document stream to PureYAML.parse(_:) |
Throws unsupportedMultiDocumentStream; use PureYAML.parseStream(_:) when multiple documents are expected. |
UnsupportedYAMLGapsFixtures.parseErrors, StreamParsingTests |
| Content after explicit document end | Throws unsupportedMultiDocumentStream; trailing comments after ... remain non-content. |
UnsupportedYAMLGapsFixtures |
Unknown directives such as %FOO |
Throws unsupportedDirective. |
UnsupportedYAMLGapsFixtures.parseErrors |
%TAG after content or after document start |
Throws unsupportedDirective. |
UnsupportedYAMLGapsFixtures.parseErrors |
| Undefined aliases | Throws undefinedAlias. |
CollectionCompatibilityFixtures.parseErrors |
| Invalid explicit scalar tag values | Throws invalidTaggedScalar. |
ScalarCompatibilityFixtures.invalidExplicitScalarTags |
| Invalid merge values | Throws invalidMergeValue when << is not followed by a mapping or sequence of mappings. |
MergeKeyCompatibilityTests |
Unsupported built-in tags such as !!timestamp, !!binary, !!set, and !!omap |
parse(_:) keeps parseable value trees without special tag construction. parseTagged(_:) preserves the tags, and tagged validation reports them as unsupported built-in tags. |
UnsupportedYAMLGapsFixtures.fallbackValues, TaggedCompatibilityTests |
| Sexagesimal-style scalars and date-looking plain scalars | Keep string values for currently unsupported spellings such as 1:20, 2002-04-28, and 09. |
ScalarCompatibilityFixtures.unsupportedScalars |
| Custom resolvers and environment expansion | Not implemented. Build project-specific conversion on top of Model.Value, tagged nodes, typed Codable, or PureYAML.Tagged.Constructor. |
Future compatibility work |
Foundation-specific scalar conversions such as Data, Date, or URL strategies |
Not special-cased in the library target. Tags stay visible through parseTagged(_:); callers own conversion. |
TaggedCompatibilityTests |
Migration Checklist
- Audit the YAML corpus the caller actually reads and writes.
- Classify each document against the supported and unsupported tables above.
- Add representative fixtures for project-specific documents before switching call sites.
- Parse through
PureYAML.parse(_:)and runPureYAML.validate(_:)before typed decoding when duplicate-key visibility matters. - Prefer typed
Codablefor known schemas andPureYAML.Model.Valuefor inspection, migration tooling, or unsupported-shape triage. - Run macOS, Linux, Windows, and WASM gates before publishing the migration.
When Not to Migrate Yet
Do not migrate a caller yet if it requires:
- binary or timestamp conversion into Foundation types;
- arbitrary
Anyload and dump APIs; - custom resolvers;
- a full YAML 1.2 compliance claim.
In those cases, add a fixture and a child issue before changing production call sites.