Skip to content

Releases: sorunokoe/SwiftMapper

2.13.0 — Flat keyword initializer + Rule-field fix

Choose a tag to compare

@sorunokoe sorunokoe released this 10 Aug 11:45

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

Choose a tag to compare

@sorunokoe sorunokoe released this 06 Aug 14:22

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 Bench executable 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.md explains 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

Choose a tag to compare

@sorunokoe sorunokoe released this 06 Aug 10:00

Simplify and shorten the README (documentation only, no code changes).

1.1.3

Choose a tag to compare

@sorunokoe sorunokoe released this 01 Aug 11:43

What's new

  • New diagnostic: default-valued let properties are now detected and reported as a compile-time error.
    A stored let property with an in-place default value (e.g. let id: UUID = .init()) can never be assigned by any additional initializer -- not even by self.id = ... or a whole-self reassignment like self = 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 does self = creation(...).
    @Mapper now 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 testDiagnosesDefaultValuedStoredProperty regression test.

1.1.2

Choose a tag to compare

@sorunokoe sorunokoe released this 01 Aug 11:29

Fixes

  • Preserve type-level attributes (e.g. @MainActor, @Sendable) on function-typed fields when generating a @Mapper builder. Previously the ownership-specifier fix in 1.1.1 over-stripped and also discarded these, silently breaking global-actor-isolated closure fields.
  • @escaping/@autoclosure are now correctly kept on the generated buildBlock's own parameter (needed to forward into the canonical initializer) while still excluded from Boxed<T>'s generic argument (where they're invalid).

New

  • @Mapper now 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 to Equatable/Hashable/Comparable that 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

Choose a tag to compare

@sorunokoe sorunokoe released this 01 Aug 11:08

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

Choose a tag to compare

@sorunokoe sorunokoe released this 31 Jul 11:05

Highlights

  • Branching: if/else and switch can now appear directly inside a
    builder closure — no more hoisting to a switch-expression/helper function
    outside the closure. The generated builder implements buildEither(first:)
    / buildEither(second:) plus a generic single-field buildBlock.
    • Optional-typed fields need an explicit else (producing nil) rather
      than a bare if — see README's Branching section
      for why (buildOptional isn't implemented; it would double-wrap an
      already-Optional field's value).
  • 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.

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.

1.0.0

Choose a tag to compare

@sorunokoe sorunokoe released this 31 Jul 09:06

Initial public release: @Mapper macro for generating composable, concise builder-style initializers for Swift structs, plus the shared Boxed<T> wrapper.