Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,16 +174,17 @@ jobs:
"swift-syntax": "601.0.1"
}
},
{
"os": "ubuntu-latest",
"swift": "latest"
},
{
"os": "macos-15",
"swift": "6.1.2"
}
]
}
# disable until prebuilt macro issues are fixed
# {
# "os": "ubuntu-latest",
# "swift": "latest"
# },

cocoapods-test:
name: CocoaPods
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
.plugin(name: "MetaProtocolCodable", targets: ["MetaProtocolCodable"]),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", "509.1.0"..<"602.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", "509.1.0"..<"603.0.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"),
Expand All @@ -47,6 +47,7 @@ let package = Package(
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
]
),
.target(name: "MetaCodable", dependencies: ["MacroPlugin"]),
Expand Down
3 changes: 2 additions & 1 deletion [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ let package = Package(
.plugin(name: "MetaProtocolCodable", targets: ["MetaProtocolCodable"]),
],
dependencies: [
.package(url: "https://github.com/swiftlang/swift-syntax.git", "509.1.0"..<"602.0.0"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", "509.1.0"..<"603.0.0"),
.package(url: "https://github.com/apple/swift-collections.git", from: "1.0.4"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2"),
],
Expand All @@ -46,6 +46,7 @@ let package = Package(
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
]
),
.target(name: "MetaCodable", dependencies: ["MacroPlugin"]),
Expand Down
2 changes: 1 addition & 1 deletion Sources/HelperCoders/ValueCoders/Number.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
protocol NumberCodingStrategy: ValueCodingStrategy where Value == Self {}

public extension ValueCodingStrategy
where Value: Decodable & ExpressibleByIntegerLiteral & LosslessStringConvertible
where Value: Decodable & ExpressibleByIntegerLiteral & LosslessStringConvertible & Sendable
{
/// Decodes numeric data from the given `decoder`.
///
Expand Down
16 changes: 8 additions & 8 deletions Sources/MacroPlugin/Definitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ struct UnTagged: PeerMacro {
/// implementation depending on the type of attached declaration:
/// * `struct`/`class`/`enum`/`actor` types: Expansion of `Decodable`
/// protocol conformance members.
public struct ConformDecodable: MemberMacro, ExtensionMacro {
struct ConformDecodable: MemberMacro, ExtensionMacro {
/// Expand to produce members for `Decodable`.
///
/// Membership macro expansion for `ConformDecodable` macro
Expand All @@ -719,7 +719,7 @@ public struct ConformDecodable: MemberMacro, ExtensionMacro {
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Delegated member expansion from `PluginCore.ConformDecodable`.
public static func expansion(
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
Expand All @@ -743,7 +743,7 @@ public struct ConformDecodable: MemberMacro, ExtensionMacro {
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Delegated member expansion from `PluginCore.ConformDecodable`.
public static func expansion(
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
Expand All @@ -769,7 +769,7 @@ public struct ConformDecodable: MemberMacro, ExtensionMacro {
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Delegated extension expansion from `PluginCore.ConformDecodable`.
public static func expansion(
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
Expand All @@ -791,7 +791,7 @@ public struct ConformDecodable: MemberMacro, ExtensionMacro {
/// implementation depending on the type of attached declaration:
/// * `struct`/`class`/`enum`/`actor` types: Expansion of `Encodable`
/// protocol conformance members.
public struct ConformEncodable: MemberMacro, ExtensionMacro {
struct ConformEncodable: MemberMacro, ExtensionMacro {
/// Expand to produce members for `Encodable`.
///
/// Membership macro expansion for `ConformEncodable` macro
Expand All @@ -803,7 +803,7 @@ public struct ConformEncodable: MemberMacro, ExtensionMacro {
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Delegated member expansion from `PluginCore.ConformEncodable`.
public static func expansion(
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
Expand All @@ -827,7 +827,7 @@ public struct ConformEncodable: MemberMacro, ExtensionMacro {
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Delegated member expansion from `PluginCore.ConformEncodable`.
public static func expansion(
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
Expand All @@ -853,7 +853,7 @@ public struct ConformEncodable: MemberMacro, ExtensionMacro {
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Delegated extension expansion from `PluginCore.ConformEncodable`.
public static func expansion(
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
Expand Down
6 changes: 1 addition & 5 deletions Sources/MetaCodable/MetaCodable.docc/Limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,13 @@ enum SomeEnum {
}
```

### Why enums with raw value aren't supported?

`Swift` compiler by default generates `Codable` conformance for `enum`s with raw value and `MetaCodable` has nothing extra to add for these type of `enum`s. Hence, in this case the default compiler generated implementation can be used.

### Why actor conformance to Encodable not generated?

For `actor`s ``Codable(commonStrategies:)`` generates `Decodable` conformance, while `Encodable` conformance isn't generated, only `encode(to:)` method implementation is generated which is isolated to `actor`.

To generate `Encodable` conformance, the `encode(to:)` method must be `nonisolated` to `actor`, and since `encode(to:)` method must be synchronous making it `nonisolated` will prevent accessing mutable properties.

Due to these limitations, `Encodable` conformance isn't generated, users has to implement the conformance manually.
Due to these limitations, `Encodable` conformance isn't generated, users have to implement the conformance manually.

### Why MetaProtocolCodable plugin can't scan Xcode target dependencies?

Expand Down
1 change: 0 additions & 1 deletion Sources/MetaCodable/MetaCodable.docc/MetaCodable.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Supercharge `Swift`'s `Codable` implementations with macros.

`MetaCodable` framework exposes custom macros which can be used to generate dynamic `Codable` implementations. The core of the framework is ``Codable(commonStrategies:)`` macro which generates the implementation aided by data provided with using other macros.


`MetaCodable` aims to supercharge your `Codable` implementations by providing these inbox features:

- Allows custom `CodingKey` value declaration per variable with ``CodedAt(_:)`` passing single argument, instead of requiring you to write all the `CodingKey` values.
Expand Down
9 changes: 7 additions & 2 deletions Sources/PluginCore/Attributes/CodedBy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,13 @@ package struct CodedBy: PropertyAttribute {
isEnum || isProtocol,
AggregatedDiagnosticProducer {
mustBeCombined(with: Codable.self)
mustBeCombined(
with: CodedAt.self, or: DecodedAt.self, EncodedAt.self
`if`(
isRawRepresentableEnum,
mustBeCombined(with: Codable.self),
else: mustBeCombined(
with: CodedAt.self,
or: DecodedAt.self, EncodedAt.self
)
)
},
else: AggregatedDiagnosticProducer {
Expand Down
51 changes: 45 additions & 6 deletions Sources/PluginCore/Attributes/KeyPath/CodedAt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,13 @@ where Var == ExternallyTaggedEnumSwitcher, Decl == EnumDeclSyntax {
///
/// If valid key paths are found, creates an `InternallyTaggedEnumSwitcher` with
/// the specified configuration. The identifier type is determined from the `CodedAs`
/// attribute if present, otherwise defaults to `String`.
/// attribute if present, otherwise defaults to `String`. The `topDecode` and `topEncode`
/// flags are determined based on whether the respective key paths are empty.
///
/// - Parameters:
/// - container: The container token for case variation encoding/decoding.
/// - decl: The enum declaration syntax for which code is being generated.
/// - coderPrefix: The prefix for coder variable names that will be used
/// to generate decoder and encoder variable names.
/// - identifier: The identifier token name to use for tagging.
/// - codingKeys: The coding keys map for key path resolution.
/// - forceDecodingReturn: Whether to force explicit `return` statements in
Expand All @@ -103,7 +106,8 @@ where Var == ExternallyTaggedEnumSwitcher, Decl == EnumDeclSyntax {
/// applying both builder functions. Otherwise, returns the current registration
/// with a type-erased variable, indicating external tagging should be used.
func checkForInternalTagging<Variable, Switcher>(
container: TokenSyntax, identifier: TokenSyntax,
decl: EnumDeclSyntax,
coderPrefix: TokenSyntax, identifier: TokenSyntax,
codingKeys: CodingKeysMap, forceDecodingReturn: Bool,
context: some MacroExpansionContext,
variableBuilder: @escaping (
Expand All @@ -120,23 +124,58 @@ where Var == ExternallyTaggedEnumSwitcher, Decl == EnumDeclSyntax {
let path = tagAttr?.keyPath(withExisting: []) ?? []
let decodedPath = decodeTagAttr?.keyPath(withExisting: path) ?? path
let encodedPath = encodeTagAttr?.keyPath(withExisting: path) ?? path
let rawRepresentable =
decl.inheritanceClause?
.inheritedTypes.contains { $0.type.isRawValueType } ?? false
&& decl.codableMembers(input: codingKeys).allSatisfy { member in
member.element.parameterClause == nil
}

guard
!decodedPath.isEmpty && !encodedPath.isEmpty
(!decodedPath.isEmpty && !encodedPath.isEmpty) || rawRepresentable
else { return self.updating(with: variable.any) }
let typeAttr = CodedAs(from: decl)
let keyPath = PathKey(decoding: decodedPath, encoding: encodedPath)
let variable = InternallyTaggedEnumSwitcher(
identifierDecodeContainer: container,
identifierEncodeContainer: container,
coderPrefix: coderPrefix, topDecode: keyPath.decoding.isEmpty,
topEncode: keyPath.encoding.isEmpty,
identifier: identifier, identifierType: typeAttr?.type,
keyPath: keyPath, codingKeys: codingKeys,
decl: decl, context: context,
forceDecodingReturn: forceDecodingReturn,
rawRepresentable: rawRepresentable,
variableBuilder: variableBuilder
)

let newRegistration = switcherBuilder(self.updating(with: variable))
return newRegistration.updating(with: newRegistration.variable.any)
}
}

extension TypeSyntax {
/// Determines if this type syntax represents a raw value type suitable for enum raw values.
///
/// Raw value types are fundamental Swift types that can be used as the underlying
/// raw value type for enums. This includes all integer types, floating-point types,
/// boolean, string, and character types that Swift supports natively.
///
/// The supported types include:
/// - **Boolean**: `Bool`
/// - **String types**: `String`, `Character`
/// - **Signed integers**: `Int`, `Int8`, `Int16`, `Int32`, `Int64`, `Int128`
/// - **Unsigned integers**: `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64`, `UInt128`
/// - **Floating-point**: `Float`, `Float16`, `Float80`, `Double`
///
/// This property is used to determine if an enum can be treated as RawRepresentable
/// and whether it should use raw value-based decoding/encoding strategies.
///
/// - Returns: `true` if the type is a valid raw value type, `false` otherwise.
var isRawValueType: Bool {
[
"Bool", "String", "Character",
"Int", "Int8", "Int16", "Int32", "Int64", "Int128",
"UInt", "UInt8", "UInt16", "UInt32", "UInt64", "UInt128",
"Float", "Float16", "Float80", "Double",
].contains(trimmedDescription)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import SwiftSyntax

/// Validates provided syntax is a raw representable enum.
///
/// Checks if the provided enum declaration has:
/// 1. An inheritance clause with a raw value type (String, Int, etc.)
/// 2. All enum cases have no associated values (parameter clauses)
struct RawRepresentableEnumCondition: DiagnosticCondition {
/// Determines whether provided syntax passes validation.
///
/// This type checks the provided syntax with current data for validation.
/// Checks if the syntax is an enum declaration that conforms to RawRepresentable
/// by having a raw value type and no associated values in its cases.
///
/// - Parameter syntax: The syntax to validate.
/// - Returns: Whether syntax passes validation.
func satisfied(by syntax: some SyntaxProtocol) -> Bool {
guard let enumDecl = syntax.as(EnumDeclSyntax.self) else {
return false
}

// Check if enum has inheritance clause with raw value type
let hasRawValueType =
enumDecl.inheritanceClause?
.inheritedTypes.contains { $0.type.isRawValueType } ?? false

// Check if all enum cases have no associated values
let allCasesHaveNoAssociatedValues = enumDecl.memberBlock.members
.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
.allSatisfy { caseDecl in
caseDecl.elements.allSatisfy { element in
element.parameterClause == nil
}
}

return hasRawValueType && allCasesHaveNoAssociatedValues
}
}

extension Attribute {
/// Whether declaration is a raw representable enum.
///
/// Uses `RawRepresentableEnumCondition` to check if the enum has a raw value type
/// and no associated values in its cases.
var isRawRepresentableEnum: RawRepresentableEnumCondition { .init() }
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ struct BasicEnumCaseVariable: EnumCaseVariable {
/// - Parameters:
/// - decl: The declaration to read data from.
/// - context: The context in which to perform the macro expansion.
/// - node: The node at which variables are registered.
/// - switcher: The switcher variable for handling enum case variations.
/// - builder: The builder action to use to update associated variables
/// registration data.
///
Expand Down
Loading
Loading