Skip to content

SwiftXMLCoder 1.0.0

Choose a tag to compare

@MFranceschi6 MFranceschi6 released this 15 Mar 23:18
· 52 commits to main since this release

[1.0.0] — 2026-03-16

Added (Epic H — Pre-Release API Completeness)

  • H.1 — userInfo on XMLEncoder and XMLDecoder: XMLEncoder.Configuration and XMLDecoder.Configuration now accept userInfo: [CodingUserInfoKey: Any] (default [:]). Both internal types _XMLTreeEncoder and _XMLTreeDecoder now return options.userInfo from their Encoder/Decoder protocol property, enabling context injection into custom Encodable/Decodable implementations. Both Configuration structs changed from Sendable to @unchecked Sendable (required by [CodingUserInfoKey: Any]; same approach as Foundation.JSONEncoder).
  • H.2a — NilEncodingStrategy fix for synthesised optionals: _XMLKeyedEncodingContainer now overrides all 15 concrete encodeIfPresent(_:forKey:) overloads (Bool, String, Double, Float, Int, Int8, Int16, Int32, Int64, UInt, UInt8, UInt16, UInt32, UInt64, and generic T) plus the generic encodeIfPresent<T: Encodable>. Previously, Swift's synthesised encode(to:) bypassed encodeNil(forKey:) entirely by calling the concrete encodeIfPresent overloads, making NilEncodingStrategy ineffective for compiler-synthesised Encodable conformances. All overloads delegate to _encodeIfPresent which routes nil through encodeNil(forKey:).
  • H.3 — XMLKeyTransformStrategy: New XMLKeyTransformStrategy enum (useDefaultKeys, convertToSnakeCase, convertToKebabCase, capitalized, uppercased, lowercased, custom(@Sendable (String) -> String)). Added to XMLEncoder.Configuration and XMLDecoder.Configuration (default .useDefaultKeys). Encoder applies the transform to key.stringValue before creating child element/attribute names; decoder applies the same transform to key.stringValue before performing lookups in the tree (transform-then-match strategy — no inverse required). Attributes (@XMLAttribute) respect the same transform. New file Sources/SwiftXMLCoder/XMLKeyTransformStrategy.swift.
  • H.4a — StringEncodingStrategy (global CDATA): New XMLEncoder.StringEncodingStrategy enum (.text default, .cdata) added to XMLEncoder.Configuration. When .cdata, all String values are wrapped in CDATA sections (<![CDATA[...]]>). _XMLTreeElementBox gains appendCDATA(_:) and a .cdata(String) case on _XMLTreeContentBox, mapped to XMLTreeNode.cdata in makeElement(). The decode path already handled CDATA transparently via XMLTreeParser.
  • H.4b — @XMLCDATA macro (per-field CDATA): New @XMLCDATA() peer macro; XMLStringCodingOverrideProvider protocol; XMLStringEncodingHint enum (.text, .cdata). @XMLCodable now synthesises XMLStringCodingOverrideProvider conformance (xmlPropertyStringHints) when at least one property is annotated @XMLCDATA. The encoder resolves: per-property hint → global stringEncodingStrategy → .text. Attributes annotated with @XMLCDATA compile but the hint is silently ignored (CDATA is not valid in attributes). New files: Sources/SwiftXMLCoder/XMLStringEncodingHint.swift, Sources/SwiftXMLCoderMacroImplementation/XMLCDATAMacro.swift.
  • H.5 — Whitespace policy discoverability: XMLDecoder gains a public var whitespacePolicy: XMLTreeParser.WhitespaceTextNodePolicy computed property as a convenience accessor for configuration.parserConfiguration.whitespaceTextNodePolicy (previously buried 2 levels deep).
  • H.6a — expandEmptyElements in XMLTreeWriter: XMLTreeWriter.Configuration gains expandEmptyElements: Bool (default false). When true, child-less elements are emitted as <tag></tag> instead of <tag/> by injecting an empty text node before serialisation. Implemented without XML_SAVE_NO_EMPTY (a libxml2 global flag); instead, the writer injects an empty text node per element — libxml2 omits self-closing only when an element has at least one child.
  • H.6b — @XMLExpandEmpty macro (per-field expand-empty): New @XMLExpandEmpty() peer macro; XMLExpandEmptyProvider protocol (xmlPropertyExpandEmptyKeys: Set<String>). @XMLCodable now synthesises XMLExpandEmptyProvider when at least one property is annotated @XMLExpandEmpty. The encoder injects an empty text node into the child element box after encoding, if the box has no content and the key is in xmlPropertyExpandEmptyKeys. The empty text node is dropped by the parser's default whitespaceTextNodePolicy: .dropWhitespaceOnly, so decoded values remain semantically unchanged. New files: Sources/SwiftXMLCoder/XMLExpandEmptyProvider.swift, Sources/SwiftXMLCoderMacroImplementation/XMLExpandEmptyMacro.swift.
  • @XMLCodable macro declaration updated: conformances list now includes XMLFieldCodingOverrideProvider, XMLDateCodingOverrideProvider, XMLStringCodingOverrideProvider, XMLExpandEmptyProvider; names list now includes all four synthesised property names.
  • 42 new tests: XMLKeyTransformStrategyTests (12), XMLCDATAMacroIntegrationTests (6), XMLExpandEmptyMacroIntegrationTests (5), XMLEncoderTests H.1 (2) / H.2a (2) / H.4a (4), XMLDecoderTests H.1 (2), XMLTreeParserWriterTests H.6a (4).

Added

  • XMLValidationPolicy: build-time and runtime configurable validation policy. Default is .lenient (no validation); .strict enables element-name and XSD temporal validation. Set at build time via -DSWIFT_XML_CODER_STRICT_VALIDATION; override per-instance via XMLEncoder.Configuration.validationPolicy / XMLDecoder.Configuration.validationPolicy.

  • XSD temporal value types: XMLGYear, XMLGYearMonth, XMLGMonth, XMLGDay, XMLGMonthDay, XMLTime, XMLDuration — all Sendable, Equatable, Hashable, Codable (encode/decode as XSD lexical strings). Each type with a Foundation.Date bridge where meaningful.

  • XMLTimezoneOffset: value type for XSD timezone offsets (Z, ±HH:MM); public static let utc.

  • TimeZone.utc: convenience public extension.

  • DateEncodingStrategy new cases: .xsdDate(timeZone:), .xsdTime(timeZone:), .xsdGYear(timeZone:), .xsdGYearMonth(timeZone:), .xsdGMonth(timeZone:), .xsdGDay(timeZone:), .xsdGMonthDay(timeZone:) — encode Foundation.Date as each XSD partial-date form.

  • DateDecodingStrategy new cases: .xsdDate, .xsdTime, .xsdGYear, .xsdGYearMonth, .xsdGMonth, .xsdGDay, .xsdGMonthDay — decode XSD partial-date lexical strings into Foundation.Date.

  • _XMLTemporalFoundationSupport.formatXSDDate(_:timeZone:) and parseXSDDate(_:) helpers for xs:date ↔ Foundation.Date conversion.

  • 61 new tests in XMLTemporalTypesTests covering parsing, roundtrip, invalid input, Foundation.Date bridges, and XMLValidationPolicy build-time/runtime behavior.

  • XMLDocument.ParsingConfiguration.untrusted() static factory that explicitly enforces all libxml2 hardening flags (.forbidNetwork, DTD forbidden, entity references preserved, blank text nodes trimmed). Intended for use with XMLTreeParser.Limits.untrustedInputDefault() for full defence-in-depth against malicious XML input.

  • 3 new tests in XMLTreeHardeningTests covering untrusted() policy assertions, rejection of a deeply-nested XML bomb, and rejection of an oversized text node.

  • XMLRootNameResolver.explicitRootElementName(from:validationPolicy:) and implicitRootElementName(for:validationPolicy:) now accept a validationPolicy parameter. In .strict mode, a rootElementName or XMLRootNode.xmlRootElementName that requires sanitization fails with [XML6_6_ROOT_NAME_INVALID] instead of silently correcting.

  • _XMLEncoderOptions.init now throws [XML6_6_ITEM_NAME_INVALID] in .strict mode when itemElementName requires sanitization. In .lenient mode (default) the name is still sanitized silently.

  • 8 new tests in XMLEncoderTests (D.1 matrix): strict rejection of rootElementName with space, digit-prefix, invalid XMLRootNode name, and itemElementName with space; lenient sanitization pass for root and item names; valid-name no-throw assertions.

  • Tests/.swiftlint.yml: raised type_body_length warning threshold to 600 lines for test files (test classes grow with coverage matrix).

  • New test target SwiftXMLCoderMacroTests (Swift 5.9+ manifests): uses SwiftSyntaxMacrosTestSupport.assertMacroExpansion to verify macro diagnostic IDs and expansion output. Covers: @XMLCodable on enum → XML8A_INVALID_DECL error; @XMLCodable on actor → same error; @XMLCodable on struct/class → correct xmlFieldNodeKinds expansion; @XMLAttribute/@XMLChild as pure peer markers (no generated peers).

  • @XMLDateFormat(_ hint: XMLDateFormatHint) macro (Swift 5.9+ only): per-property date format annotation for stored properties of type Date or Date?. Apply inside a type annotated with @XMLCodable to override the global dateEncodingStrategy/dateDecodingStrategy for that property.

  • XMLDateFormatHint enum: Sendable, Equatable, Hashable, Codable. Covers all XSD date strategies: .xsdDate, .xsdTime, .xsdDateTime, .xsdGYear, .xsdGYearMonth, .xsdGMonth, .xsdGDay, .xsdGMonthDay, .xsdDateWithTimezone(identifier:), .xsdTimeWithTimezone(identifier:), .secondsSince1970, .millisecondsSince1970. Converts to XMLEncoder.DateEncodingStrategy and XMLDecoder.DateDecodingStrategy via .encodingStrategy and .decodingStrategy computed properties.

  • XMLDateCodingOverrideProvider protocol: synthesised by @XMLCodable when any property is annotated with @XMLDateFormat. The static xmlPropertyDateHints: [String: XMLDateFormatHint] dictionary is consulted by encoder and decoder before the global strategy.

  • @XMLCodable now synthesises a second extension (XMLDateCodingOverrideProvider) when at least one @XMLDateFormat annotation is present. The xmlPropertyDateHints dictionary maps property names to their hints verbatim.

  • XMLEncoder and XMLDecoder now apply per-property date hints from XMLDateCodingOverrideProvider before the instance-level strategy, for both top-level types and all nested types.

  • 14 new integration tests in XMLDateFormatMacroIntegrationTests: hint → strategy conversions, per-property encode override, per-property decode override, roundtrip, global fallback, Codable roundtrip for all XMLDateFormatHint cases.

  • 4 new macro expansion tests in XMLMacroDiagnosticsTests: @XMLDateFormat as pure peer marker; @XMLCodable with @XMLDateFormat → xmlPropertyDateHints extension; mixed @XMLAttribute/@XMLDateFormat; complex parameterised hint passed verbatim.

  • XMLCapturingLogHandler in SwiftXMLCoderTestSupport: thread-safe LogHandler implementation that captures all log entries for assertion in tests. Provides entries, entries(at:), entries(containing:), hasEntry(at:containing:), hasEntry(at:withMetadataKey:), and reset() helpers. Enables white-box verification that log messages are emitted at the correct levels with the correct metadata.

  • XMLEncoder.Configuration and XMLDecoder.Configuration now accept a logger: Logger parameter (default Logger(label: "SwiftXMLCoder")). The logger is propagated through _XMLEncoderOptions and _XMLDecoderOptions to all call sites.

  • XMLTreeParser.Configuration now accepts a logger: Logger parameter; the logger is passed to XMLDocument.init for libxml2 diagnostics and used for structured parse-phase logging.

  • XMLEncoder emits .debug at encode start (type, rootElement) and completion (rootElement, childCount). Emits .warning when rootElementName or XMLRootNode.xmlRootElementName is silently sanitized in lenient mode. Emits .debug when root element name is derived from type name.

  • XMLDecoder emits .debug at decode start (type, rootElement) and completion (rootElement, childCount). Emits .error before throwing XML6_5_ROOT_MISMATCH (with expected, found, type metadata).

  • XMLTreeParser emits .debug on parse start and completion (with nodeCount). Emits .warning when any limit is exceeded (with code, context, actual, limit metadata). Emits .warning once when node count or element depth first reaches 80% of the configured cap — subsequent nodes at the same level do not re-emit.

  • Encoder and decoder emit .trace (not .debug) when a per-property date hint from XMLDateCodingOverrideProvider overrides the global strategy (with field and hint metadata).

  • 20 new integration tests in XMLStructuredLoggingTests covering encoder/decoder/parser debug lifecycle entries with metadata assertions, root name sanitization warnings, root mismatch error log, per-property hint trace entries, limit exceeded/approaching warnings (including once-only guarantee), and XMLCapturingLogHandler helpers.

  • iOS 15+, tvOS 15+, watchOS 8+ platform support (Epic G.1 — Track 1): libxml2 resolves via Xcode SDK sysroot; pkgConfig silently no-ops on device/simulator SDKs. No source changes required. CI adds ios-simulator build-only job (arm64-apple-ios15.0-simulator).

  • Platforms badge and Requirements table in README.md updated to include iOS, tvOS, watchOS.

  • DocC Articles/Compatibility.md documents new Apple platform targets and CI gate policy.

Changed

  • XML field-name validation (error code XML6_6_FIELD_NAME_INVALID) is now gated by XMLValidationPolicy.validateElementNames. Existing tests updated to use validationPolicy: .strict to preserve their intent. In lenient mode (default) invalid field names are silently passed through to libxml2.

Breaking Changes

  • Renamed XMLElement<Value> property wrapper to XMLChild<Value>. The old name is retained as a @available(*, deprecated, renamed: "XMLChild") typealias for source compatibility.
  • Renamed @XMLElement macro to @XMLChild. The old macro name is kept as a deprecated alias pointing to the same implementation (XMLChildMacro).
  • Removed preserveWhitespaceTextNodes: Bool parameter from XMLTreeParser.Configuration.init and Configuration.untrustedInputProfile. Use whitespaceTextNodePolicy: .preserve instead.
  • XMLCanonicalizationError.code now returns XMLCanonicalizationErrorCode instead of String. Update callers that compare .code to raw string literals to use the typed constant (e.g. .code == .transformFailed).

Added

  • XMLNamespace conforms to Equatable and Hashable.
  • XMLParsingError conforms to Equatable. Two .other values are never equal (existential payload cannot be compared structurally — documented in the case's doc comment).
  • makeXMLSafeName now strips namespace prefixes from qualified names (e.g. "soap:Envelope" → "Envelope" instead of "soap_Envelope"). Regression test added.
  • Regression test test_encodeTree_itemElementName_namespacePrefixed_stripsPrefix.
  • DocC catalog (Sources/SwiftXMLCoder/SwiftXMLCoder.docc/): landing page + 8 articles (GettingStarted, FieldMapping, Namespaces, Canonicalization, XPath, Security, Compatibility, TestSupport).
  • README.md: installation instructions (all 3 SPM product variants), quick-start code examples, feature matrix, and links to DocC articles.
  • Inline /// doc comments on all previously undocumented public types and members: XMLNode, XMLNamespaceDeclaration, XMLNormalizationOptions, XMLTemporalCoding types (XMLDateFormatterDescriptor, XMLDateCodingContext, XMLDateEncodingClosure, XMLDateDecodingClosure), XMLCanonicalizationErrorCode (all static constants), XMLNamespace, XMLQualifiedName, XMLTreeNode (all cases), XMLTreeAttribute, XMLCanonicalizationError, XMLCanonicalizationStage, XMLCanonicalView, XMLIdentityTransform, XMLDefaultCanonicalizer, XMLNamespaceResolver, XMLNamespaceValidator, XMLDocument (createElement, appendChild, serializedData, XPath methods).
  • GitHub Actions CI workflow (.github/workflows/ci.yml): matrix build and test across Linux (Swift 5.6, 5.9, 5.10, 6.0, 6.1) and macOS 15 (Swift 6.1 / Xcode 16.2). Coverage report generated via llvm-cov and uploaded as artifact on pushes to main.
  • GitHub Actions SwiftLint workflow (.github/workflows/lint.yml): runs realm/SwiftLint@v0.57.1 with PR review annotations on every push and pull request to main.
  • GitHub Actions DocC workflow (.github/workflows/docc.yml): builds documentation for the SwiftXMLCoder target on macOS; will enforce --warnings-as-errors once the DocC catalog is added in Epic B.
  • Tests/.swiftlint.yml: nested SwiftLint config relaxing identifier_name (min 1) and type_name (min 1) for test files, and demoting force_unwrapping from error to warning in tests. Source code remains at full strictness.