Releases: sorunokoe/SwiftMapper
Release list
2.13.0 — Flat keyword initializer + Rule-field fix
New: flat keyword initializer (2.12.0)
@Mapper now generates a third member for every attached struct/class: a plain, labeled "keyword" initializer that mirrors the canonical initializer's own labels, order, and types exactly, with each field wrapped in an independent () -> T closure — an ordinary Swift call instead of the existing Builder-DSL block:
// Builder-DSL (unchanged, still generated):
Address { Street, City, PostalCode in
Street { rawInput.line1 }
City { rawInput.city.capitalized }
PostalCode { rawInput.zip.trimmed() }
}
// New: flat keyword initializer
Address(
street: { rawInput.line1 },
city: { rawInput.city.capitalized },
postalCode: { rawInput.zip.trimmed() }
)Both initializers are always generated together; neither replaces the other. Purely additive — no breaking changes.
Fix: no .execute() needed for Rule fields in the keyword initializer (2.13.0)
The keyword initializer's per-field Rule resolution was redesigned to use a private, per-position RuleBuilder<T> typealias attached to each field's closure parameter, instead of requiring an explicit .execute() call. Each field independently resolves a plain value or a child Rule via RuleBuilder's existing buildExpression overloads.
This also avoids a recurrence of a known Swift compiler bug (swiftlang/swift#70087) where a type's hand-written == (or other operator) alongside an arbitrary-named member macro gets misreported as colliding with itself. The fix uses a fixed, statically-named list of typealiases (via named(...)), at the cost of a documented, diagnosed 32-field cap on the keyword initializer only — the Builder-DSL initializer is unaffected.
Upgrading
No source changes required — this is a drop-in upgrade. If you generate a diagnostic about exceeding 32 fields in the keyword initializer, switch that initializer's call to the Builder-DSL form instead.
2.11.0 — Zero-parse macro codegen + benchmarking
Performance
@Mapper's generated builder init and nested Builder enum are now built entirely out of typed SwiftSyntax nodes instead of interpolated Swift-source strings fed through DeclSyntax(stringLiteral:). Previously, expanding @Mapper forced the compiler to re-parse that generated text on every single expansion, with cost scaling with each mapped type's field count and type complexity. That re-parsing is now gone entirely — no text is generated or re-parsed at macro-expansion time.
Generated output is byte-for-byte unchanged: verified against the existing 996-line exact-expansion-string test suite (MapperMacroExpansionTests.swift) with zero test changes needed. No breaking changes.
Incidentally fixes a pre-existing MemberMacro deprecation warning encountered while touching this code.
Benchmarking
- New dev-only
Benchexecutable target (swift run -c release Bench) measures the@Mapper-generated builder initializer against a hand-written initializer and a plain memberwise initializer, confirming the builder stays a genuinely zero-cost runtime abstraction. - New
docs/BENCHMARKING.mdexplains both of SwiftMapper's performance dimensions — runtime (zero-cost, measured) and compile time (no re-parsing, the dimension that actually scales with project size) — and how to reproduce/track them yourself. - README gets a short new Performance section linking to the doc.
Upgrading
No source changes required — this is a drop-in upgrade.
2.10.1
1.1.3
What's new
- New diagnostic: default-valued
letproperties are now detected and reported as a compile-time error.
A storedletproperty with an in-place default value (e.g.let id: UUID = .init()) can never be assigned by any additional initializer -- not even byself.id = ...or a whole-selfreassignment likeself = other. This is a genuine Swift compiler limitation (reproduces with plain hand-written structs, no macros involved), and it always broke@Mapper's generated builder init, which doesself = creation(...).
@Mappernow detects this shape deterministically and raises a clear compile-time error at the offending property, explaining the root cause and the fix (drop the default, set it explicitly in the canonical initializer's body instead).
Docs
- Added a new "Known limitations" subsection documenting this compiler behavior, alongside the existing Equatable/Hashable/Comparable conflict subsection.
- Added a "Requirements (v1)" bullet calling out the restriction.
Tests
- 16/16 tests passing (9 macro-expansion + 7 integration), including a new
testDiagnosesDefaultValuedStoredPropertyregression test.
1.1.2
Fixes
- Preserve type-level attributes (e.g.
@MainActor,@Sendable) on function-typed fields when generating a@Mapperbuilder. Previously the ownership-specifier fix in 1.1.1 over-stripped and also discarded these, silently breaking global-actor-isolated closure fields. @escaping/@autoclosureare now correctly kept on the generatedbuildBlock's own parameter (needed to forward into the canonical initializer) while still excluded fromBoxed<T>'s generic argument (where they're invalid).
New
@Mappernow emits a best-effort warning when it detects a struct shape that's likely to hit a known, unavoidable Swift compiler bug (swiftlang/swift#70087): a struct conforming toEquatable/Hashable/Comparablethat already hand-writes its own==/</hash(into:)(typically because a field, like a function type, isn't itself Equatable). See the README's new "Known limitations" section for details and the recommended workaround.
No breaking changes.
1.1.1 — Fix ownership-specifier parameter support
Bugfix release.
Initializer parameters with ownership specifiers (consuming, borrowing) now work correctly with @Mapper. Previously, an initializer like:
init(value: consuming String) { self.value = value }generated invalid code (Boxed<consuming String>) that failed to compile with a cryptic "cannot infer contextual base in reference to member 'init'" error, since ownership specifiers are only valid on a function parameter declaration, not as a generic argument.
No breaking changes — fully additive/corrective.
1.1.0 — Branching support and improved diagnostics
Highlights
- Branching:
if/elseandswitchcan now appear directly inside a
builder closure — no more hoisting to a switch-expression/helper function
outside the closure. The generated builder implementsbuildEither(first:)
/buildEither(second:)plus a generic single-fieldbuildBlock.- Optional-typed fields need an explicit
else(producingnil) rather
than a bareif— see README's Branching section
for why (buildOptionalisn't implemented; it would double-wrap an
already-Optional field's value).
- Optional-typed fields need an explicit
- Better diagnostics:
- Multiple-initializer errors are now reported once per extra
initializer, each at its own location, instead of a single diagnostic
on the kept initializer. - Unlabeled-parameter errors (
_ name: String) now come with a Fix-It
that promotes the internal name to the label, plus a clearer message.
- Multiple-initializer errors are now reported once per extra
See the updated README for full usage, the branching caveats,
and the complete list of diagnostics.
Compatibility
Fully additive — no breaking changes to the @Mapper macro's existing
generated API. Existing consumers can upgrade from 1.0.0 with no code
changes required.