Skip to content

Commit 04e47f5

Browse files
Export enum (#476)
Export enumeration support Any enumeration that is blessed with `CaseIterable` can be then exported to Godot by using `@Export(.enum)` on it, like this: ``` enum MyEnumeration: Int, CaseIterable { case first case second } ``` To export, use the `.enum` parameter to Export: ``` @godot class Demo: Node { @export(.enum) var myState: MyEnumeration } ``` One limitation of the current change is that this works by using `CaseIterable`, either by manually typing it, or using the `PickerNameProvider` macro.
1 parent e7e8de3 commit 04e47f5

File tree

8 files changed

+228
-43
lines changed

8 files changed

+228
-43
lines changed

Generator/Generator/Enums.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func generateEnums (_ p: Printer, cdef: JClassInfo?, values: [JGodotGlobalEnumEl
8585
}
8686
let extraConformances = enumDefName == "Error" ? ", Error" : ""
8787

88-
p ("public enum \(getGodotType (SimpleType (type: enumDefName))): Int64, CustomDebugStringConvertible\(extraConformances)") {
88+
p ("public enum \(getGodotType (SimpleType (type: enumDefName))): Int64, CaseIterable, CustomDebugStringConvertible\(extraConformances)") {
8989
var used = Set<Int> ()
9090

9191
func getName (_ enumVal: JGodotValueElement) -> String? {

Sources/SwiftGodot/SwiftGodot.docc/Exports.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,22 @@ To surface arrays in Godot, use a strong type for it, for example:
288288
@Export
289289
var myResources: VariantCollection<Resource>
290290
```
291+
292+
### Enumeration Values
293+
294+
To surface enumeration values, use the `@Export(.enum)` marker on your variable,
295+
and it is important that your enumeration conforms to `CaseIterable`, like this:
296+
297+
```
298+
enum MyEnum: CaseIterable {
299+
case first
300+
case second
301+
}
302+
303+
@Godot
304+
class Sample: Node {
305+
@Export(.enum)
306+
var myValue: MyEnum
307+
}
308+
```
309+

Sources/SwiftGodotMacroLibrary/MacroExport.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// File.swift
2+
// MacroExport.swift
33
//
44
//
55
// Created by Miguel de Icaza on 9/25/23.
@@ -15,8 +15,17 @@ import SwiftSyntaxMacros
1515
public struct GodotExport: PeerMacro {
1616

1717

18-
static func makeGetAccessor (varName: String, isOptional: Bool) -> String {
18+
static func makeGetAccessor (varName: String, isOptional: Bool, isEnum: Bool) -> String {
1919
let name = "_mproxy_get_\(varName)"
20+
if isEnum {
21+
return
22+
"""
23+
func \(name) (args: [Variant]) -> Variant? {
24+
return Variant (\(varName).rawValue)
25+
}
26+
"""
27+
28+
}
2029
if isOptional {
2130
return
2231
"""
@@ -35,11 +44,18 @@ public struct GodotExport: PeerMacro {
3544
}
3645
}
3746

38-
static func makeSetAccessor (varName: String, typeName: String, isOptional: Bool) -> String {
47+
static func makeSetAccessor (varName: String, typeName: String, isOptional: Bool, isEnum: Bool) -> String {
3948
let name = "_mproxy_set_\(varName)"
4049
var body: String = ""
4150

42-
if typeName == "Variant" {
51+
if isEnum {
52+
body =
53+
"""
54+
if let iv = Int (args [0]), let ev = \(typeName)(rawValue: numericCast (iv)) {
55+
self.\(varName) = ev
56+
}
57+
"""
58+
} else if typeName == "Variant" {
4359
body = "\(varName) = args [0]"
4460
} else if godotVariants [typeName] == nil {
4561
let optBody = isOptional ? " else { \(varName) = nil }" : ""
@@ -113,6 +129,14 @@ public struct GodotExport: PeerMacro {
113129
throw GodotMacroError.requiresNonOptionalGArrayCollection
114130
}
115131

132+
var isEnum = false
133+
if case let .argumentList (arguments) = node.arguments, let expression = arguments.first?.expression {
134+
isEnum = expression.description.trimmingCharacters(in: .whitespacesAndNewlines) == ".enum"
135+
}
136+
if isEnum && isOptional {
137+
throw GodotMacroError.noSupportForOptionalEnums
138+
139+
}
116140
var results: [DeclSyntax] = []
117141

118142
for singleVar in varDecl.bindings {
@@ -162,8 +186,8 @@ public struct GodotExport: PeerMacro {
162186
results.append (DeclSyntax(stringLiteral: makeGArrayCollectionGetProxyAccessor(varName: varName, elementTypeName: elementTypeName)))
163187
results.append (DeclSyntax(stringLiteral: makeGArrayCollectionSetProxyAccessor(varName: varName, elementTypeName: elementTypeName)))
164188
} else if let typeName = type.as(IdentifierTypeSyntax.self)?.name.text {
165-
results.append (DeclSyntax(stringLiteral: makeSetAccessor(varName: varName, typeName: typeName, isOptional: isOptional)))
166-
results.append (DeclSyntax(stringLiteral: makeGetAccessor(varName: varName, isOptional: isOptional)))
189+
results.append (DeclSyntax(stringLiteral: makeSetAccessor(varName: varName, typeName: typeName, isOptional: isOptional, isEnum: isEnum)))
190+
results.append (DeclSyntax(stringLiteral: makeGetAccessor(varName: varName, isOptional: isOptional, isEnum: isEnum)))
167191
}
168192
}
169193

Sources/SwiftGodotMacroLibrary/MacroGodot.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,11 @@ class GodotMacroProcessor {
204204
ctor.append ("\tclassInfo.registerMethod(name: StringName(\"\(funcName)\"), flags: .default, returnValue: \(retProp ?? "nil"), arguments: \(funcArgs == "" ? "[]" : "\(funcName)Args"), function: \(className)._mproxy_\(funcName))\n")
205205
}
206206

207-
func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws {
207+
// Returns true if it used "tryCase"
208+
func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws -> Bool {
209+
var usedTryCase = false
208210
guard hasExportAttribute(varDecl.attributes) else {
209-
return
211+
return false
210212
}
211213
guard let last = varDecl.bindings.last else {
212214
throw GodotMacroError.noVariablesFound
@@ -232,6 +234,13 @@ class GodotMacroProcessor {
232234
guard let ips = singleVar.pattern.as(IdentifierPatternSyntax.self) else {
233235
throw GodotMacroError.expectedIdentifier(singleVar)
234236
}
237+
guard let last = varDecl.bindings.last else {
238+
throw GodotMacroError.noVariablesFound
239+
}
240+
guard let ta = last.typeAnnotation?.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) else {
241+
throw GodotMacroError.noTypeFound(varDecl)
242+
}
243+
235244
let varNameWithPrefix = ips.identifier.text
236245
let varNameWithoutPrefix = String(varNameWithPrefix.trimmingPrefix(prefix ?? ""))
237246
let proxySetterName = "_mproxy_set_\(varNameWithPrefix)"
@@ -275,16 +284,24 @@ class GodotMacroProcessor {
275284
}
276285
}
277286
}
278-
let propType = godotTypeToProp (typeName: typeName)
287+
let mappedType = godotTypeToProp (typeName: typeName)
279288
let pinfo = "_p\(varNameWithPrefix)"
289+
let isEnum = firstLabeledExpression?.description == "enum"
290+
291+
292+
let propType = isEnum ? ".int" : mappedType
293+
let fallback = isEnum ? "tryCase (\(ta).self)" : "\"\""
294+
if isEnum {
295+
usedTryCase = true
296+
}
280297
ctor.append (
281298
"""
282299
let \(pinfo) = PropInfo (
283300
propertyType: \(propType),
284301
propertyName: "\(varNameWithPrefix)",
285302
className: className,
286303
hint: .\(firstLabeledExpression?.description ?? "none"),
287-
hintStr: \(secondLabeledExpression?.description ?? "\"\""),
304+
hintStr: \(secondLabeledExpression?.description ?? fallback),
288305
usage: .default)
289306
290307
""")
@@ -293,6 +310,10 @@ class GodotMacroProcessor {
293310
ctor.append("\tclassInfo.registerMethod (name: \"\(setterName)\", flags: .default, returnValue: nil, arguments: [\(pinfo)], function: \(className).\(proxySetterName))\n")
294311
ctor.append("\tclassInfo.registerProperty (\(pinfo), getter: \"\(getterName)\", setter: \"\(setterName)\")\n")
295312
}
313+
if usedTryCase {
314+
return true
315+
}
316+
return false
296317
}
297318

298319
func processGArrayCollectionVariable(_ varDecl: VariableDeclSyntax, prefix: String?) throws {
@@ -402,7 +423,7 @@ class GodotMacroProcessor {
402423
"""
403424
var previousGroupPrefix: String? = nil
404425
var previousSubgroupPrefix: String? = nil
405-
426+
var needTrycase = false
406427
for member in classDecl.memberBlock.members.enumerated() {
407428
let decl = member.element.decl
408429

@@ -420,12 +441,23 @@ class GodotMacroProcessor {
420441
if varDecl.isGArrayCollection {
421442
try processGArrayCollectionVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
422443
} else {
423-
try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
444+
if try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix) {
445+
needTrycase = true
446+
}
424447
}
425448
} else if let macroDecl = MacroExpansionDeclSyntax(decl) {
426449
try classInitSignals(macroDecl)
427450
}
428451
}
452+
if needTrycase {
453+
ctor.append (
454+
"""
455+
func tryCase <T : RawRepresentable & CaseIterable> (_ type: T.Type) -> GString {
456+
GString (type.allCases.map { v in "\\(v):\\(v.rawValue)" }.joined(separator: ","))
457+
}
458+
func tryCase <T : RawRepresentable> (_ type: T.Type) -> String { "" }
459+
""")
460+
}
429461
ctor.append("} ()\n")
430462
return ctor
431463
}

Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ enum GodotMacroError: Error, DiagnosticMessage {
4848
case expectedIdentifier(PatternBindingListSyntax.Element)
4949
case unknownError(Error)
5050
case unsupportedCallableEffect
51+
case noSupportForOptionalEnums
5152

5253
var severity: DiagnosticSeverity {
5354
return .error
@@ -77,6 +78,8 @@ enum GodotMacroError: Error, DiagnosticMessage {
7778
"@Export optional Collections are not supported"
7879
case .unsupportedCallableEffect:
7980
"@Callable does not support asynchronous or throwing functions"
81+
case .noSupportForOptionalEnums:
82+
"@Export(.enum) does not support optional values for the enumeration"
8083
}
8184
}
8285

Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public struct PickerNameProviderMacro: ExtensionMacro {
2828
case .notAnEnum:
2929
return "@PickerNameProvider can only be applied to an 'enum'"
3030
case .missingInt:
31-
return "@PickerNameProvider requires an Int backing"
31+
return "@PickerNameProvider requires an Int64 backing"
3232
}
3333
}
3434

@@ -58,12 +58,6 @@ public struct PickerNameProviderMacro: ExtensionMacro {
5858
let types = inheritors.map { $0.type.as(IdentifierTypeSyntax.self) }
5959
let names = types.map { $0?.name.text }
6060

61-
guard names.contains("Int") else {
62-
let missingInt = Diagnostic(node: declaration.root, message: ProviderDiagnostic.missingInt)
63-
context.diagnose(missingInt)
64-
return []
65-
}
66-
6761
let members = enumDecl.memberBlock.members
6862
let cases = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
6963
let elements = cases.flatMap { $0.elements }
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//
2+
// MacroGodotExportEnumTests.swift
3+
// SwiftGodotMacrosTests
4+
//
5+
// Created by Estevan Hernandez on 11/29/23.
6+
//
7+
8+
import SwiftSyntaxMacros
9+
import SwiftSyntaxMacrosTestSupport
10+
import XCTest
11+
import SwiftGodotMacroLibrary
12+
13+
final class MacroGodotExportEnumTests: XCTestCase {
14+
let testMacros: [String: Macro.Type] = [
15+
"Godot": GodotMacro.self,
16+
"Export": GodotExport.self,
17+
]
18+
19+
func testExportEnumGodot() {
20+
assertMacroExpansion(
21+
"""
22+
enum Demo: Int, CaseIterable {
23+
case first
24+
}
25+
enum Demo64: Int64, CaseIterable {
26+
case first
27+
}
28+
@Godot
29+
class SomeNode: Node {
30+
@Export(.enum) var demo: Demo
31+
@Export(.enum) var demo64: Demo64
32+
}
33+
""",
34+
expandedSource:
35+
"""
36+
enum Demo: Int, CaseIterable {
37+
case first
38+
}
39+
enum Demo64: Int64, CaseIterable {
40+
case first
41+
}
42+
class SomeNode: Node {
43+
var demo: Demo
44+
45+
func _mproxy_set_demo (args: [Variant]) -> Variant? {
46+
if let iv = Int (args [0]), let ev = Demo(rawValue: numericCast (iv)) {
47+
self.demo = ev
48+
}
49+
return nil
50+
}
51+
52+
func _mproxy_get_demo (args: [Variant]) -> Variant? {
53+
return Variant (demo.rawValue)
54+
}
55+
var demo64: Demo64
56+
57+
func _mproxy_set_demo64 (args: [Variant]) -> Variant? {
58+
if let iv = Int (args [0]), let ev = Demo64(rawValue: numericCast (iv)) {
59+
self.demo64 = ev
60+
}
61+
return nil
62+
}
63+
64+
func _mproxy_get_demo64 (args: [Variant]) -> Variant? {
65+
return Variant (demo64.rawValue)
66+
}
67+
68+
override open class var classInitializer: Void {
69+
let _ = super.classInitializer
70+
return _initializeClass
71+
}
72+
73+
private static var _initializeClass: Void = {
74+
let className = StringName("SomeNode")
75+
assert(ClassDB.classExists(class: className))
76+
let classInfo = ClassInfo<SomeNode> (name: className)
77+
let _pdemo = PropInfo (
78+
propertyType: .int,
79+
propertyName: "demo",
80+
className: className,
81+
hint: .enum,
82+
hintStr: tryCase (Demo.self),
83+
usage: .default)
84+
classInfo.registerMethod (name: "_mproxy_get_demo", flags: .default, returnValue: _pdemo, arguments: [], function: SomeNode._mproxy_get_demo)
85+
classInfo.registerMethod (name: "_mproxy_set_demo", flags: .default, returnValue: nil, arguments: [_pdemo], function: SomeNode._mproxy_set_demo)
86+
classInfo.registerProperty (_pdemo, getter: "_mproxy_get_demo", setter: "_mproxy_set_demo")
87+
let _pdemo64 = PropInfo (
88+
propertyType: .int,
89+
propertyName: "demo64",
90+
className: className,
91+
hint: .enum,
92+
hintStr: tryCase (Demo64.self),
93+
usage: .default)
94+
classInfo.registerMethod (name: "_mproxy_get_demo64", flags: .default, returnValue: _pdemo64, arguments: [], function: SomeNode._mproxy_get_demo64)
95+
classInfo.registerMethod (name: "_mproxy_set_demo64", flags: .default, returnValue: nil, arguments: [_pdemo64], function: SomeNode._mproxy_set_demo64)
96+
classInfo.registerProperty (_pdemo64, getter: "_mproxy_get_demo64", setter: "_mproxy_set_demo64")
97+
func tryCase <T : RawRepresentable & CaseIterable> (_ type: T.Type) -> GString {
98+
GString (type.allCases.map { v in
99+
"\\(v):\\(v.rawValue)"
100+
} .joined(separator: ","))
101+
}
102+
func tryCase <T : RawRepresentable> (_ type: T.Type) -> String {
103+
""
104+
}
105+
} ()
106+
}
107+
""",
108+
macros: testMacros
109+
)
110+
}
111+
}

0 commit comments

Comments
 (0)