How to implement a macro that generates a large RawRepresentable Enum?

Assume we have an enum like this:

enum Currency: String {
case usd = "USD"
case eur = "EUR"
case `try` = "TRY"
... 100 more lines ...
}

This enum is helpful when dealing with ISO-4217 currency codes, and may be generated fully automatically (which I've done via simple swift script).
Trying to implements a macro that could generate enum without any need to call any shell script build phases.
Can't figure out how can I achieve that. Tried different approaches:

  1. freestanding macro that simply returns a string that then is converted to ExprSyntax
public struct CurrenciesGenerateMacro: ExpressionMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {
        return """
               enum Currency: String {
               case adp = "ADP"
               case aed = "AED"
               }
               """
    }
}

(actually it's a bit more complicated, with a lookup to NSLocale.isoCurrencyCodes, but there's some weird error with is, I will mention it down below)

and the compiler warns me this doesn't work:

  1. tried to make an attached(member, names: arbitrary). but the compiler warns me there are no cases in enum for it to be a RawRepresentable:
@GenerateCurrencies
enum Currency: String { // <- 'Currency' declares raw type 'String', but does not conform to RawRepresentable and conformance could not be synthesized

}

but expanding a macro show all the cases are generated inside :thinking:

Also, when debugging my implementation, I've faced with an error I cannot clarify for me:

What is wrong here, hence the macro seemed to expand everything correctly?
The macro code is:

extension String {
    func wrappedInTicksIfNeeded() -> String {
        let keywords: Set<String> = ["for", "try", "let", "var"]
        return keywords.contains(self) ? "`\(self)`" : self
    }
}

public struct CurrenciesGenerateMacro: ExpressionMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {

        var result = "enum Currency: String {"

        NSLocale.isoCurrencyCodes.forEach { code in
            if let description = NSLocale.current.localizedString(forCurrencyCode: code) {
                result += "\n    /// \(description)"
            } else {
                result += "\n"
            }

            result += "\n    case \(code.lowercased().wrappedInTicksIfNeeded()) = \"\(code)\"\n"
        }

        result += "}"

        return ExprSyntax(stringLiteral: result)
    }
}

Tried to eliminate any parsing issues via tree builder, but cannot figure out how to return a tree as an ExprSyntax:

public struct CurrenciesGenerateMacro: ExpressionMacro {
    public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> SwiftSyntax.ExprSyntax {

let syntax = TypeSyntax(
            EnumDeclSyntax(name: .identifier("Currency"),
                           inheritanceClause: InheritanceClauseSyntax(inheritedTypes: InheritedTypeListSyntax {
                InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("String")))
            }),
                           memberBlock: MemberBlockSyntax {
                MemberBlockItemListSyntax {
                    for code in NSLocale.isoCurrencyCodes {
                        MemberBlockItemSyntax(decl:
                                                EnumCaseDeclSyntax(leadingTrivia: Trivia.lineComment(NSLocale.current.localizedString(forCurrencyCode: code) ?? code.uppercased())) {
                            EnumCaseElementListSyntax {
                                EnumCaseElementSyntax(name: .identifier(code.lowercased().wrappedInTicksIfNeeded()),
                                                      rawValue: InitializerClauseSyntax(value: StringLiteralExprSyntax(content: code)))
                            }
                        }
                        )
                    }
                }
            })

return ??
}

Why are you wrapping the EnumDeclSyntax into a TypeSyntax here? A type definition is a DeclSyntax, and TypeSyntax is type name, function signature, tuple type, etc.

IIRC currently top-level declarations by macro is forbidden, so you’ll need to use an attached MemberMacro to generate the members. You should return a list of EnumCaseDeclSyntax in a [DeclSyntax] in this case.

Thank you, will try [DeclSyntax].
As to MemberMacro - I mentioned it in the second scenario, the compiler is not allowing me to declare a RawRepresentable enum with no cases, even when a macro expands all the members correctly

Please double check the documentation for macro kinds and their corresponding function type. The compiler won’t expand the attribute or expression correctly if the types are mismatched.

Should the compiler pick the appropriate type when constructing from string literal?

  1. Could you tolerate "case UDD, EUR, ...", etc instead?

But I do a bigger picture question: I wonder what that would give you...

  1. The currency list is not getting changed frequently. If you are using this enum from your app in e.g. a switch, and add a new case (by whatever means) you'd need to add the corresponding case handling in the switch (unless you are having a default statement with, say, fatalError("TODO") in it, which will bring your attention to the issue at runtime so you'd still need to add a new case handler). And if you add a new case manually what has stopped you adding the case manually to the enum itself in the first place?

  2. Say you do have that enum in the app (manual or autogenerated). Check how many places you actually use "adp" or "aed" in the app – are there any at all? I guess there would be none... Typically apps don't need such enums.

Unless this is an exercise on how to use macros I don't see a point...

  1. will try that, but not sure it's possible (like the try case that I need to wrap in ticks)
  2. it's actually is updating annually, which is why it's autogenerated in shellscript right now. Most of the time I don't need to add a corresponding case, since I need this enum mostly to ensure the ISO-4217 code I got from backend is correct and may be used for price formatting later. (see my library for this)
  3. I have at least rub, usd and eur in my code for a fallback scenarios

Late reply, but you need a declaration macro to introduce new type definitions.

Like this:

@freestanding(declaration, names: named(Currency))
public macro generateCurrenciesEnum() = #externalMacro(
    module: "CurrencyCodeMacroMacros", 
    type: "GenerateCurrenciesEnumMacro"
)
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

public struct GenerateCurrenciesEnumMacro: DeclarationMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> [DeclSyntax] {
        return [
            """
            enum Currency: String, CaseIterable {
                case adp = "ADP"
                case aed = "AED"
            }
            """
        ]
    }
}

@main
struct CurrencyCodeMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        GenerateCurrenciesEnumMacro.self,
    ]
}

Usage:

import CurrencyCodeMacro

#generateCurrenciesEnum()

Just out of curiosity, what's the purpose of this versus a normal Swift package?
Either way, you're invoking SPM – and doing this via a macro, with all the additional ceremony, feels like using a sledgehammer to crack a nut, so to speak.

First of all, to find out how macros work
Second, to automate this enum at compiletime without need in any script phase

Ah, fair enough on the learning point.

What benefit do you get from compile time generation of this enum versus just putting it in a separate Swift package, though? That seems like a more appropriate use for something like this, and then you won't even need to incur the huge cost of building Swift Syntax.

It’s already a cocoapods pod, but country and currency codes are not that static as they seem. Nearly each major iOS release this enum is generated differently. So, I could use some stencil template with Sourcery. But this task looks like a candidate for swift macro