Home

Awesome

<p align="center"> <a href="https://github.com/MahdiBM/enumerator-macro/actions/workflows/tests.yml"> <img src="https://github.com/MahdiBM/enumerator-macro/actions/workflows/tests.yml/badge.svg" alt="Tests Badge"> </a> <a href="https://app.codecov.io/gh/MahdiBM/enumerator-macro"> <img src="https://codecov.io/github/mahdibm/enumerator-macro/branch/main/graph/badge.svg" alt="Code Coverage"> </a> <a href="https://swift.org"> <img src="https://img.shields.io/badge/swift-6.0%20%2F%205.10-brightgreen.svg" alt="Latest/Minimum Swift Version"> </a> </p>

EnumeratorMacro

A utility for creating case-by-case code for your Swift enums using the Mustache templating engine.
EnumeratorMacro uses swift-mustache's flavor.

The macro will parse your enum code, and pass different info of your enum to the mustache template renderer.
Then you can access each case-name, case-parameters etc.. in the template, and create code based on that.

How Does Mustache Templating Work?

It's rather simple.

General Behavior

<details> <summary> Click to expand </summary>

EnumeratorMacro will:

</details>

Examples

[!NOTE] The examples will get more advanced as you scroll.

Derive Case Names

@Enumerator("""
var caseName: String {
    switch self {
    {{#cases}}
    case .{{name}}: "{{snakeCased(name)}}"
    {{/cases}}
    }
}
""")
enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)
}

Is expanded to:

enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)

+    var caseName: String {
+        switch self {
+        case .a: "a"
+        case .b: "b"
+        case .testCase: "test_case"
+        }
+    }
}

Create a Subtype Enum

<details> <summary> Click to expand </summary>
@Enumerator("""
enum Subtype: String {
    {{#cases}}
    case {{name}}
    {{/cases}}
}
""",
"""
var subtype: Subtype {
    switch self {
    {{#cases}}
    case .{{name}}:
        .{{name}}
    {{/cases}}
    }
}
""")
enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)
}

Is expanded to:

enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)

+    enum Subtype: String {
+        case a
+        case b
+        case testCase
+    }

+    var subtype: Subtype {
+        switch self {
+        case .a:
+            .a
+        case .b:
+            .b
+        case .testCase:
+            .testCase
+        }
+    }
}
</details>

Create Is-Case Properties

<details> <summary> Click to expand </summary>
@Enumerator("""
{{#cases}}
var is{{capitalized(name)}}: Bool {
    switch self {
    case .{{name}}: return true
    default: return false
    }
}
{{/cases}}
""")
enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)
}

Is expanded to:

enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)

+    var isA: Bool {
+        switch self {
+        case .a: return true
+        default: return false
+        }
+    }

+    var isB: Bool {
+        switch self {
+        case .b: return true
+        default: return false
+        }
+    }

+    var isTestCase: Bool {
+        switch self {
+        case .testCase: return true
+        default: return false
+        }
+    }
}
</details>

Create Get-Case-Value Functions

<details> <summary> Click to expand </summary>
@Enumerator("""
{{#cases}}
{{^isEmpty(parameters)}}
func get{{capitalized(name)}}() -> ({{joined(tupleValue(parameters))}})? {
    switch self {
    case let .{{name}}{{withParens(joined(names(parameters)))}}:
        return {{withParens(joined(names(parameters)))}}
    default:
        return nil
    }
}
{{/isEmpty(parameters)}}
{{/cases}}
""")
enum TestEnum {
    case a(val1: String, Int)
    case b
    case testCase(testValue: String)
}

Is expanded to:

enum TestEnum {
    case a(val1: String, val2: Int)
    case b
    case testCase(testValue: String)


+    func getA() -> (val1: String, param2: Int)? {
+        switch self {
+        case let .a(val1, param2):
+            return (val1, param2)
+        default:
+            return nil
+        }
+    }

+    func getTestCase() -> (String)? {
+        switch self {
+        case let .testCase(testValue):
+            return (testValue)
+        default:
+            return nil
+        }
+    }
}
</details>

Create Functions For Each Case

<details> <summary> Click to expand </summary>
@Enumerator("""
{{#cases}}
{{^isEmpty(parameters)}}
func get{{capitalized(name)}}() -> ({{joined(tupleValue(parameters))}})? {
    switch self {
    case let .{{name}}{{withParens(joined(names(parameters)))}}:
        return {{withParens(joined(names(parameters)))}}
    default:
        return nil
    }
}
{{/isEmpty(parameters)}}
{{/cases}}
""")
enum TestEnum {
    case a(val1: String, Int)
    case b
    case testCase(testValue: String)
}

Is expanded to:

enum TestEnum {
    case a(val1: String, Int)
    case b
    case testCase(testValue: String)

+    func getA() -> (val1: String, param2: Int)? {
+        switch self {
+        case let .a(val1, param2):
+            return (val1, param2)
+        default:
+            return nil
+        }
+    }

+    func getTestCase() -> (String)? {
+        switch self {
+        case let .testCase(testValue):
+            return (testValue)
+        default:
+            return nil
+        }
+    }
}
</details>

Using Comments For Code Generation

[!NOTE] You can use comments in front of each case, as values for EnumeratorMacro to process.
Use ; to divide the comments, and use : to separate the key and the possible value.
Example: myKey1; myKey2: value; myKey3.

[!TIP] You should declare a allowedComments argument to enforce that only certain comments are used, so you avoid typo bugs.

@Enumerator(
    allowedComments: ["business_error", "l8n_params"],
    """
    package var isBusinessError: Bool {
        switch self {
        case
        {{#cases}}{{#bool(business_error(comments))}}
        .{{name}},
        {{/bool(business_error(comments))}}{{/cases}}
        :
            return true
        default:
            return false
        }
    }
    """
)
public enum ErrorMessage {
    case case1 // business_error
    case case2 // business_error: true
    case case3 // business_error: false
    case case4 // business_error: adfasdfdsff
    case somethingSomething(value: String)
    case otherCase(error: Error, isViolation: Bool) // business_error; l8n_params:
}

Is expanded to:

public enum ErrorMessage {
    case case1 // business_error
    case case2 // business_error: true
    case case3 // business_error: false
    case case4 // business_error: adfasdfdsff
    case somethingSomething(value: String)
    case otherCase(error: Error, isViolation: Bool) // business_error; l8n_params:

+    package var isBusinessError: Bool {
+        switch self {
+        case
+        .case1,
+        .case2,
+        .otherCase
+        :
+            return true
+        default:
+            return false
+        }
+    }
}

Advanced Using Comments For Code Generation

<details> <summary> Click to expand </summary>
@Enumerator(
    allowedComments: ["business_error", "l8n_params"],
    """
    private var localizationParameters: [Any] {
        switch self {
        {{#cases}}
    {{! Only create a case for enum cases that have any parameters at all: }}
        {{^isEmpty(parameters)}}
    
    {{! Create a case for those who have non-empty 'l8n_params' comment: }}
        {{^isEmpty(l8n_params(comments))}}
        case let .{{name}}{{withParens(joined(names(parameters)))}}:
            [{{l8n_params(comments)}}]
        {{/isEmpty(l8n_params(comments))}}
    
    {{! Create a case for those who don't have 'l8n_params' comment at all: }}
        {{^exists(l8n_params(comments))}}
        case let .{{name}}{{withParens(joined(names(parameters)))}}:
            [
                {{#parameters}}
                {{name}}{{#isOptional}} as Any{{/isOptional}},
                {{/parameters}}
            ]
        {{/exists(l8n_params(comments))}}
    
        {{/isEmpty(parameters)}}
        {{/cases}}
        default:
            []
        }
    }
    """
)
public enum ErrorMessage {
    case case1 // business_error
    case case2 // business_error: true
    case case3 // business_error: false
    case case4 // business_error: adfasdfdsff
    case somethingSomething(value1: String, Int) // l8n_params: value
    case otherCase(error: Error, isViolation: Bool) // business_error; l8n_params:
}

Is expanded to:

public enum ErrorMessage {
    case case1 // business_error
    case case2 // business_error: true
    case case3 // business_error: false
    case case4 // business_error: adfasdfdsff
    case somethingSomething(value1: String, Int) // l8n_params: value
    case otherCase(error: Error, isViolation: Bool) // business_error; l8n_params:

    private var localizationParameters: [Any] {
        switch self {
        case .somethingSomething:
            [value]
        default:
            []
        }
    }
}
</details>

Available Context Values

Here's a sample context object:

{
    "cases": [
        {
            "name": "somethingWentWrong",
            "parameters": [
                {
                    "name": "error",
                    "type": "Error?",
                    "isOptional": true,
                    "index": 0,
                    "isFirst": true,
                    "isLast": false
                },
                {
                    "name": "statusCode",
                    "type": "String",
                    "isOptional": false,
                    "index": 1,
                    "isFirst": false,
                    "isLast": true
                }
            ],
            "comments": [
                {
                    "key": "business_error",
                    "value": ""
                },
                {
                    "key": "l8n_params",
                    "value": "error as Any, statusCode"
                }
            ],
            "index": 0,
            "isFirst": true,
            "isLast": true
        }
    ]
}

Available Functions

Although not visible when writing templates, each underlying value that is passed to the template engine has an actual type.

EnumeratorMacro supports these transformations for each type:

Feel free to suggest a function if you think it'll solve a problem.

Error Handling

<details> <summary> Click to expand </summary>

In case there is an error in the expanded macro code, or in any other step of the code generation, EnumeratorMacro will try to emit diagnostics pointing to the line of the code which is the source of the issue.

For example, EnumeratorMacro will properly forward template render errors from the template engine to your source code. In the example below, I've mistakenly written {{cases} instead of {{cases}}:

<kbd> <img width="767" alt="Screenshot 2024-07-13 at 12 09 16 AM" src="https://github.com/user-attachments/assets/6763cfd4-b435-4ffb-adc9-03912b09a3b3"> </kbd>

Or here, the expanded Swift code would result in a code with syntax error and the macro is preemprively reporting the error. Here, I've supposedly forgot to write the : between caseName and String.

<kbd> <img width="768" alt="Screenshot 2024-07-13 at 12 10 19 AM" src="https://github.com/user-attachments/assets/97f177a4-e5a0-437f-b3f9-3e9ef902e744"> </kbd>

EnumeratorMacro can also catch an invalid function being used:

<kbd> <img width="824" alt="Screenshot 2024-07-17 at 6 44 41 PM" src="https://github.com/user-attachments/assets/d664830d-1897-4099-86b2-1c32d3f6def1"> </kbd>

</details>

How To Add EnumeratorMacro To Your Project

To use the EnumeratorMacro library in a SwiftPM project, add the following line to the dependencies in your Package.swift file:

.package(url: "https://github.com/MahdiBM/enumerator-macro", branch: "main"),

Include EnumeratorMacro as a dependency for your targets:

.target(name: "<target>", dependencies: [
    .product(name: "EnumeratorMacro", package: "enumerator-macro"),
]),

Finally, add import EnumeratorMacro to your source code.