diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift index 0d3f032..0e348a3 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift @@ -7,36 +7,29 @@ import Foundation -/** - A macro that makes enum types polymorphically codable. - - This macro adds Codable conformance to enum types, allowing them to be serialized and - deserialized based on a type identifier. It generates the necessary coding keys, - initializer, and encoding methods. - - Each enum case must have exactly one associated value, and the type of that value must conform - to `PolymorphicIdentifiable`. - - - Parameters: - - identifierCodingKey: The key name in the JSON used to store the type identifier. - The default value for this property is `"type"`. This key is used to identify the specific - case of the enum during - - fallbackCaseName: The name of the `case` to use when the type identifier is not found. - The default value for this property is `nil`. If this property is not provided, the macro will - throw an error if the type identifier is not found. - - - Warning: When decoding falls back to the fallback case, during encoding the original `type` value - will not be preserved. Instead, the `type` value of the fallback case will be used in the - encoded output. - */ -@attached( - extension, - conformances: Codable, - names: named(PolymorphicMetaCodingKey), - named(init), - named(encode) -) +/// A macro that makes enum types polymorphically codable. +/// +/// This macro adds Codable conformance to enum types, allowing them to be serialized and +/// deserialized based on a type identifier. It generates the necessary coding keys, +/// initializer, and encoding methods. +/// +/// Each enum case must have exactly one associated value, and the type of that value must conform +/// to `PolymorphicIdentifiable`. +/// +/// - Parameters: +/// - identifierCodingKey: The key name in the JSON used to store the type identifier. +/// The default value for this property is `"type"`. This key is used to identify the specific +/// case of the enum during encoding and decoding. +/// - fallbackCaseName: The name of the `case` to use when the type identifier is not found. +/// The default value for this property is `nil`. If this property is not provided, the macro will +/// throw an error if the type identifier is not found. +/// +/// - Warning: When decoding falls back to the fallback case, during encoding the original `type` value +/// will not be preserved. Instead, the `type` value of the fallback case will be used in the +/// encoded output. +@attached(member, names: named(PolymorphicMetaCodingKey), named(init), named(encode)) +@attached(extension, conformances: Codable) public macro PolymorphicEnumCodable( identifierCodingKey: String = "type", - fallbackCaseName: String? = nil + fallbackCaseName: String? = nil, ) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumCodableMacro") diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift index 513b07e..2f7cb0b 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift @@ -7,25 +7,24 @@ import Foundation -/** - A macro that makes enum types polymorphically decodable. - - This macro adds Decodable conformance to enum types, allowing them to be deserialized - based on a type identifier. It generates the necessary coding keys and initializer methods. - - Each enum case must have exactly one associated value, and the type of that value must conform to - `PolymorphicIdentifiable`. - - - Parameters: - - identifierCodingKey: The key name in the JSON used to store the type identifier. - The default value for this property is `"type"`. This key is used to identify the specific - case of the enum during - - fallbackCaseName: The name of the `case` to use when the type identifier is not found. - The default value for this property is `nil`. If this property is not provided, the macro will - throw an error if the type identifier is not found. - */ -@attached(extension, conformances: Decodable, names: named(PolymorphicMetaCodingKey), named(init)) +/// A macro that makes enum types polymorphically decodable. +/// +/// This macro adds Decodable conformance to enum types, allowing them to be deserialized +/// based on a type identifier. It generates the necessary coding keys and initializer methods. +/// +/// Each enum case must have exactly one associated value, and the type of that value must conform to +/// `PolymorphicIdentifiable`. +/// +/// - Parameters: +/// - identifierCodingKey: The key name in the JSON used to store the type identifier. +/// The default value for this property is `"type"`. This key is used to identify the specific +/// case of the enum during decoding. +/// - fallbackCaseName: The name of the `case` to use when the type identifier is not found. +/// The default value for this property is `nil`. If this property is not provided, the macro will +/// throw an error if the type identifier is not found. +@attached(member, names: named(PolymorphicMetaCodingKey), named(init)) +@attached(extension, conformances: Decodable) public macro PolymorphicEnumDecodable( identifierCodingKey: String = "type", - fallbackCaseName: String? = nil + fallbackCaseName: String? = nil, ) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumDecodableMacro") diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift index 24e20e5..d26f09b 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift @@ -7,21 +7,20 @@ import Foundation -/** - A macro that makes enum types polymorphically encodable. - - This macro adds Encodable conformance to enum types, allowing them to be serialized - based on a type identifier. It generates the necessary coding keys and encoding methods. - - Each enum case must have exactly one associated value, and the type of that value must conform - to `PolymorphicIdentifiable`. - - - Parameters: - - identifierCodingKey: The key name in the JSON used to store the type identifier. - The default value for this property is `"type"`. This key is used to identify the specific - case of the enum during - */ -@attached(extension, conformances: Encodable, names: named(PolymorphicMetaCodingKey), named(encode)) +/// A macro that makes enum types polymorphically encodable. +/// +/// This macro adds Encodable conformance to enum types, allowing them to be serialized +/// based on a type identifier. It generates the necessary coding keys and encoding methods. +/// +/// Each enum case must have exactly one associated value, and the type of that value must conform +/// to `PolymorphicIdentifiable`. +/// +/// - Parameters: +/// - identifierCodingKey: The key name in the JSON used to store the type identifier. +/// The default value for this property is `"type"`. This key is used to identify the specific +/// case of the enum during encoding. +@attached(member, names: named(encode)) +@attached(extension, conformances: Encodable) public macro PolymorphicEnumEncodable( identifierCodingKey: String = "type" ) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumEncodableMacro") diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift index f811fac..d7f79b0 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift @@ -10,29 +10,21 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct PolymorphicEnumCodableMacro: ExtensionMacro { +public struct PolymorphicEnumCodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - // Ensure the declaration is an enum and extract case information + providingMembersOf declaration: some DeclGroupSyntax, + in _: some MacroExpansionContext, + ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw CodableKitError.message("`@PolymorphicEnumCodable` can only be attached to enums") } - // Validate and extract identifierCodingKey let identifierCodingKey = try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) - - // Extract case information from the enum let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) - - // Validate and extract fallbackCaseName if provided let fallbackCaseName = try PolymorphicEnumCodableFactory.validateFallbackCaseName( in: node, - caseInfos: caseInfos + caseInfos: caseInfos, ) let polymorphicMetaCodingKeySyntax = PolymorphicEnumCodableFactory.makePolymorphicMetaCodingKey( @@ -45,24 +37,38 @@ public struct PolymorphicEnumCodableMacro: ExtensionMacro { with: caseInfos, identifierCodingKey: identifierCodingKey, accessLevel: accessLevel, - fallbackCaseName: fallbackCaseName + fallbackCaseName: fallbackCaseName, ) let encodeToEncoderSyntax = PolymorphicEnumCodableFactory.makeEncodeToEncoder( with: caseInfos, - accessLevel: accessLevel + accessLevel: accessLevel, ) return [ - try ExtensionDeclSyntax("extension \(raw: enumDecl.name.text): Codable") { - """ - \(raw: polymorphicMetaCodingKeySyntax) + "\(raw: polymorphicMetaCodingKeySyntax)", + "\(raw: initFromDecoderSyntax)", + "\(raw: encodeToEncoderSyntax)", + ] + } +} + +extension PolymorphicEnumCodableMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in _: some MacroExpansionContext, + ) throws -> [ExtensionDeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw CodableKitError.message("`@PolymorphicEnumCodable` can only be attached to enums") + } - \(raw: initFromDecoderSyntax) + try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) + let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) + try PolymorphicEnumCodableFactory.validateFallbackCaseName(in: node, caseInfos: caseInfos) - \(raw: encodeToEncoderSyntax) - """ - }, - ] + return try [ExtensionDeclSyntax("extension \(type.trimmed): Codable {}")] } } diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift index 9c601b9..79d9340 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift @@ -6,35 +6,25 @@ // Copyright © 2025 Danggeun Market Inc. All rights reserved. // -import Foundation - import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct PolymorphicEnumDecodableMacro: ExtensionMacro { +public struct PolymorphicEnumDecodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - // Ensure the declaration is an enum and extract case information + providingMembersOf declaration: some DeclGroupSyntax, + in _: some MacroExpansionContext, + ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw CodableKitError.message("`@PolymorphicEnumDecodable` can only be attached to enums") } - // Validate and extract identifierCodingKey let identifierCodingKey = try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) - - // Extract case information from the enum let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) - - // Validate and extract fallbackCaseName if provided let fallbackCaseName = try PolymorphicEnumCodableFactory.validateFallbackCaseName( in: node, - caseInfos: caseInfos + caseInfos: caseInfos, ) let polymorphicMetaCodingKeySyntax = PolymorphicEnumCodableFactory.makePolymorphicMetaCodingKey( @@ -47,17 +37,32 @@ public struct PolymorphicEnumDecodableMacro: ExtensionMacro { with: caseInfos, identifierCodingKey: identifierCodingKey, accessLevel: accessLevel, - fallbackCaseName: fallbackCaseName + fallbackCaseName: fallbackCaseName, ) return [ - try ExtensionDeclSyntax("extension \(raw: enumDecl.name.text): Decodable") { - """ - \(raw: polymorphicMetaCodingKeySyntax) - - \(raw: initFromDecoderSyntax) - """ - }, + "\(raw: polymorphicMetaCodingKeySyntax)", + "\(raw: initFromDecoderSyntax)", ] } } + +extension PolymorphicEnumDecodableMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in _: some MacroExpansionContext, + ) throws -> [ExtensionDeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw CodableKitError.message("`@PolymorphicEnumDecodable` can only be attached to enums") + } + + try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) + let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) + try PolymorphicEnumCodableFactory.validateFallbackCaseName(in: node, caseInfos: caseInfos) + + return try [ExtensionDeclSyntax("extension \(type.trimmed): Decodable {}")] + } +} diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift index 080dc30..5ad3948 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift @@ -10,38 +10,47 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct PolymorphicEnumEncodableMacro: ExtensionMacro { +public struct PolymorphicEnumEncodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - // Ensure the declaration is an enum and extract case information + providingMembersOf declaration: some DeclGroupSyntax, + in _: some MacroExpansionContext, + ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw CodableKitError.message("`@PolymorphicEnumEncodable` can only be attached to enums") } - // Validate identifierCodingKey try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) - // Extract case information from the enum let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) - let accessLevel = AccessLevelModifier.stringValue(from: declaration) let encodeToEncoderSyntax = PolymorphicEnumCodableFactory.makeEncodeToEncoder( with: caseInfos, - accessLevel: accessLevel + accessLevel: accessLevel, ) return [ - try ExtensionDeclSyntax("extension \(raw: enumDecl.name.text): Encodable") { - """ - \(raw: encodeToEncoderSyntax) - """ - }, + "\(raw: encodeToEncoderSyntax)" ] } } + +extension PolymorphicEnumEncodableMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in _: some MacroExpansionContext, + ) throws -> [ExtensionDeclSyntax] { + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw CodableKitError.message("`@PolymorphicEnumEncodable` can only be attached to enums") + } + + try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) + _ = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) + + return try [ExtensionDeclSyntax("extension \(type.trimmed): Encodable {}")] + } +} diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift index 22f86aa..2d420cb 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift @@ -1,5 +1,5 @@ // -// PolymorphicEnumCodableEnumMacroTests.swift +// PolymorphicEnumCodableMacroTests.swift // // // Created by Elon on 10/19/24. @@ -15,10 +15,9 @@ import KarrotCodableKitMacros #endif final class PolymorphicEnumCodableMacroTests: XCTestCase { - #if canImport(KarrotCodableKitMacros) let testMacros: [String: Macro.Type] = [ - "PolymorphicEnumCodable": PolymorphicEnumCodableMacro.self, + "PolymorphicEnumCodable": PolymorphicEnumCodableMacro.self ] #endif @@ -41,9 +40,7 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Codable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -75,9 +72,12 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Codable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -103,9 +103,7 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Codable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -137,9 +135,12 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Codable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -170,11 +171,16 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { DiagnosticSpec( message: "`@PolymorphicEnumCodable` can only be attached to enums", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "`@PolymorphicEnumCodable` can only be attached to enums", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -205,11 +211,16 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Invalid polymorphic identifier: expected a non-empty string.", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -240,11 +251,16 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Polymorphic Enum cases can only have one associated value", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -275,11 +291,80 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Polymorphic Enum cases should have one associated value", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} + +// MARK: - Nested Type + +extension PolymorphicEnumCodableMacroTests { + func testPolymorphicEnumCodableMacroInsideNestedType() throws { + #if canImport(KarrotCodableKitMacros) + // given + assertMacroExpansion( + """ + enum SomeEnum { + @PolymorphicEnumCodable(identifierCodingKey: "type") + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + } + } + """, + // when + expandedSource: """ + enum SomeEnum { + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + + enum PolymorphicMetaCodingKey: CodingKey { + case `type` + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: PolymorphicMetaCodingKey.self) + let type = try container.decode(String.self, forKey: PolymorphicMetaCodingKey.type) + + switch type { + case DummyCallout.polymorphicIdentifier: + self = .callout(try DummyCallout(from: decoder)) + case DummyActionableCallout.polymorphicIdentifier: + self = .actionableCallout(try DummyActionableCallout(from: decoder)) + default: + throw PolymorphicCodableError.unableToFindPolymorphicType(type) + } + } + + public func encode(to encoder: any Encoder) throws { + switch self { + case .callout(let value): + try value.encode(to: encoder) + case .actionableCallout(let value): + try value.encode(to: encoder) + } + } + } + } + + extension SomeEnum.CalloutBadge: Codable { + } + """, + macros: testMacros, + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -311,9 +396,7 @@ extension PolymorphicEnumCodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) case undefinedCallout(DummyUndefinedCallout) - } - extension CalloutBadge: Codable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -349,9 +432,12 @@ extension PolymorphicEnumCodableMacroTests { } } } + + extension CalloutBadge: Codable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -382,11 +468,16 @@ extension PolymorphicEnumCodableMacroTests { DiagnosticSpec( message: "Missing fallback case: should be defined as `case undefinedCallout", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Missing fallback case: should be defined as `case undefinedCallout", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -417,11 +508,16 @@ extension PolymorphicEnumCodableMacroTests { DiagnosticSpec( message: "Invalid fallback case name: expected a non-empty string.", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Invalid fallback case name: expected a non-empty string.", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift index 0a94f94..163cbb2 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift @@ -15,10 +15,9 @@ import KarrotCodableKitMacros #endif final class PolymorphicEnumDecodableMacroTests: XCTestCase { - #if canImport(KarrotCodableKitMacros) let testMacros: [String: Macro.Type] = [ - "PolymorphicEnumDecodable": PolymorphicEnumDecodableMacro.self, + "PolymorphicEnumDecodable": PolymorphicEnumDecodableMacro.self ] #endif @@ -41,9 +40,7 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Decodable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -64,9 +61,12 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Decodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -92,9 +92,7 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Decodable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -115,9 +113,12 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Decodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -148,11 +149,16 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { DiagnosticSpec( message: "`@PolymorphicEnumDecodable` can only be attached to enums", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "`@PolymorphicEnumDecodable` can only be attached to enums", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -183,11 +189,16 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Invalid polymorphic identifier: expected a non-empty string.", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -218,11 +229,16 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Polymorphic Enum cases can only have one associated value", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -253,11 +269,71 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Polymorphic Enum cases should have one associated value", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} + +// MARK: - Nested Type + +extension PolymorphicEnumDecodableMacroTests { + func testPolymorphicEnumDecodableMacroInsideNestedType() throws { + #if canImport(KarrotCodableKitMacros) + // given + assertMacroExpansion( + """ + enum SomeEnum { + @PolymorphicEnumDecodable(identifierCodingKey: "type") + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + } + } + """, + // when + expandedSource: """ + enum SomeEnum { + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + + enum PolymorphicMetaCodingKey: CodingKey { + case `type` + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: PolymorphicMetaCodingKey.self) + let type = try container.decode(String.self, forKey: PolymorphicMetaCodingKey.type) + + switch type { + case DummyCallout.polymorphicIdentifier: + self = .callout(try DummyCallout(from: decoder)) + case DummyActionableCallout.polymorphicIdentifier: + self = .actionableCallout(try DummyActionableCallout(from: decoder)) + default: + throw PolymorphicCodableError.unableToFindPolymorphicType(type) + } + } + } + } + + extension SomeEnum.CalloutBadge: Decodable { + } + """, + macros: testMacros, + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -289,9 +365,7 @@ extension PolymorphicEnumDecodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) case undefinedCallout(DummyUndefinedCallout) - } - extension CalloutBadge: Decodable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -314,9 +388,12 @@ extension PolymorphicEnumDecodableMacroTests { } } } + + extension CalloutBadge: Decodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -347,11 +424,16 @@ extension PolymorphicEnumDecodableMacroTests { DiagnosticSpec( message: "Missing fallback case: should be defined as `case undefinedCallout", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Missing fallback case: should be defined as `case undefinedCallout", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -382,11 +464,16 @@ extension PolymorphicEnumDecodableMacroTests { DiagnosticSpec( message: "Invalid fallback case name: expected a non-empty string.", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Invalid fallback case name: expected a non-empty string.", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift index b22a87b..9a905cd 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift @@ -15,10 +15,9 @@ import KarrotCodableKitMacros #endif final class PolymorphicEnumEncodableMacroTests: XCTestCase { - #if canImport(KarrotCodableKitMacros) let testMacros: [String: Macro.Type] = [ - "PolymorphicEnumEncodable": PolymorphicEnumEncodableMacro.self, + "PolymorphicEnumEncodable": PolymorphicEnumEncodableMacro.self ] #endif @@ -41,9 +40,7 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Encodable { public func encode(to encoder: any Encoder) throws { switch self { case .callout(let value): @@ -55,9 +52,12 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Encodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -83,9 +83,7 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Encodable { public func encode(to encoder: any Encoder) throws { switch self { case .callout(let value): @@ -97,9 +95,54 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Encodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPolymorphicEnumEncodableMacroInsideNestedType() throws { + #if canImport(KarrotCodableKitMacros) + // given + assertMacroExpansion( + """ + enum SomeEnum { + @PolymorphicEnumEncodable(identifierCodingKey: "type") + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + } + } + """, + // when + expandedSource: """ + enum SomeEnum { + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + + public func encode(to encoder: any Encoder) throws { + switch self { + case .callout(let value): + try value.encode(to: encoder) + case .actionableCallout(let value): + try value.encode(to: encoder) + } + } + } + } + + extension SomeEnum.CalloutBadge: Encodable { + } + """, + macros: testMacros, + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -130,11 +173,16 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { DiagnosticSpec( message: "`@PolymorphicEnumEncodable` can only be attached to enums", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "`@PolymorphicEnumEncodable` can only be attached to enums", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -165,11 +213,16 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Invalid polymorphic identifier: expected a non-empty string.", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -200,11 +253,16 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Polymorphic Enum cases can only have one associated value", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -235,11 +293,16 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, - column: 1 + column: 1, + ), + DiagnosticSpec( + message: "Polymorphic Enum cases should have one associated value", + line: 1, + column: 1, ), ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform")