I often have something like ..
enum DotNetType: String, CaseIterable {
case guid = "Guid"
case int = "Int32"
case float = "Single"
case dateTime = "DateTime"
case string = "String"
init?(_ s: String) {
for v in DotNetType.allCases {
if v.rawValue == s {
self = v
return
}
}
return nil
}
however, often,
case guid = "Guid" // oh no, turns out may be "uuid", "id" or "specialId"
Obviously you can solve this by just having a switch, but it occurred to me it would be elegant if
enum DotNetType: [String], CaseIterable {
case guid = ["Guid","uuid","id"]
case int = ["Int32", "int", "Int", "integer"]
case float = ["Single", "Float", "decimal"]
case dateTime = ["DateTime", "ts", "timestamp"]
case string = "String"
Unfortunately when I try this, the errors are far beyond my understanding of the guts of enum
so I cannot make it work.
Is there a way to have the type of an enum, an array?
I can see many cases (other than just parsing string tags) where it would be handy to have an enum as an array.
If you just want to associate multiple strings to each case of an enum, and have an initialiser that can return the correct case according to those strings, you can write a macro for that.
One macro for generating the initialiser, and another for annotating each case. The usage would look like this:
@RawNamesEnum
enum DotNetType {
@RawNames("Guid", "uuid", "id")
case guid
@RawNames("Int32", "int", "Int", "integer")
case int
@RawNames("Single", "float", "decimal")
case float
}
print(DotNetType("id"))
Here's the implementation:
// declarations:
@attached(member, names: named(init))
public macro RawNamesEnum() = #externalMacro(module: "...", type: "RawNamesEnum")
@attached(peer)
public macro RawNames(_ names: String...) = #externalMacro(module: "...", type: "RawNames")
// implementation:
// this is kind of dirty, but is very convenient for throwing compile time errors
extension String: @retroactive Error {}
enum RawNamesEnum: MemberMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
throw "RawNamesEnum must be attached to an enum!"
}
var allNames: Set<String> = []
var namesByCase: [String: Set<String>] = [:]
let caseDecls = enumDecl.memberBlock.members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
for caseDecl in caseDecls {
let names = try getNames(fromCase: caseDecl)
if !allNames.intersection(names).isEmpty {
throw "Cannot contain duplicate names!"
}
allNames.formUnion(names)
namesByCase[caseDecl.elements.first!.name.text] = names
}
let initDecl = InitializerDeclSyntax(optionalMark: "?", signature: .init(parameterClause: .init {
"_ name: String"
})) {
for (caseName, rawNames) in namesByCase {
let arrayLiteral = ArrayExprSyntax(expressions: rawNames.map { ExprSyntax(StringLiteralExprSyntax(content: $0)) })
"""
if \(arrayLiteral).contains(name) {
self = .\(raw: caseName)
return
}
"""
}
"return nil"
}
return [DeclSyntax(initDecl)]
}
static func getNames(fromCase caseDecl: EnumCaseDeclSyntax) throws -> Set<String> {
var names: Set<String> = []
for case let .attribute(attrSyntax) in caseDecl.attributes {
guard attrSyntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "RawNames",
case let .argumentList(argList) = attrSyntax.arguments else { continue }
for name in argList.compactMap({ $0.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue }) {
if !names.insert(name).inserted {
throw "Cannot contain duplicate names!"
}
}
}
if names.isEmpty {
names = [caseDecl.elements.first!.name.text]
}
return names
}
}
enum RawNames: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let caseDecl = declaration.as(EnumCaseDeclSyntax.self) else {
throw "RawNames can only be applied to enum cases"
}
guard caseDecl.elements.count == 1 else {
throw "RawNames can only be applied to a single enum case declaration"
}
guard let firstCase = caseDecl.elements.first, firstCase.parameterClause == nil else {
throw "RawNames cannot be applied to cases with associated values"
}
guard case let .argumentList(argList) = node.arguments,
argList.allSatisfy({
$0.expression.as(StringLiteralExprSyntax.self)?.representedLiteralValue != nil
}) else {
throw "Names must all be string literals"
}
return []
}
}
@main
struct MyMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
RawNamesEnum.self,
RawNames.self
]
}
The raw value type of an enum cannot be an array. From the documentation,
The type of these values is specified in the raw-value type and must represent an integer, floating-point number, string, or single character. In particular, the raw-value type must conform to the
Equatable
protocol and one of the following protocols:ExpressibleByIntegerLiteral
for integer literals,ExpressibleByFloatLiteral
for floating-point literals,ExpressibleByStringLiteral
for string literals that contain any number of characters, andExpressibleByUnicodeScalarLiteral
orExpressibleByExtendedGraphemeClusterLiteral
for string literals that contain only a single character.
You can still manually conform to RawRepresentable
and set the RawValue
type to an array type. I'm not sure why you want to do this though...
enum Foo: RawRepresentable {
typealias RawValue = [String]
init?(rawValue: [String]) {
// you need to write this by hand...
}
var rawValue: [String] {
// you need to write this by hand...
}
}
Technically, you can extend the @RawNamesEnum
macro to also be an ExtensionMacro
that generates a RawRepresentable
conformance like the above, but I don't find that particularly useful.