-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 17ad0e4
Showing
10 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/configuration/registries.json | ||
.swiftpm/xcode/package.xcworkspace/ | ||
.netrc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2024 Holger Wiedemann | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"originHash" : "fbff988136e52a81e6f2cde5496a6535651ce4b7339649b7fc2ecc79119926dd", | ||
"pins" : [ | ||
{ | ||
"identity" : "swift-syntax", | ||
"kind" : "remoteSourceControl", | ||
"location" : "https://github.com/apple/swift-syntax.git", | ||
"state" : { | ||
"revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", | ||
"version" : "510.0.3" | ||
} | ||
} | ||
], | ||
"version" : 3 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// swift-tools-version: 5.10 | ||
|
||
import CompilerPluginSupport | ||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "EnumCaseLabeling", | ||
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], | ||
products: [ | ||
.library( | ||
name: "EnumCaseLabeling", | ||
targets: ["EnumCaseLabeling"] | ||
), | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-syntax.git", from: "510.0.0"), | ||
], | ||
targets: [ | ||
.macro( | ||
name: "EnumCaseLabelingMacros", | ||
dependencies: [ | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"), | ||
] | ||
), | ||
|
||
.target(name: "EnumCaseLabeling", dependencies: ["EnumCaseLabelingMacros"]), | ||
|
||
.testTarget( | ||
name: "EnumCaseLabelingTests", | ||
dependencies: [ | ||
"EnumCaseLabeling", | ||
"EnumCaseLabelingMacros", | ||
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), | ||
] | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# EnumCaseLabeling | ||
|
||
**EnumCaseLabeling** is an open source package providing macros and types to extend enumerations having cases with associated values. | ||
|
||
## Getting Started | ||
|
||
Swift 5.10 is required as a minimum version. | ||
|
||
To use the `EnumCaseLabeling` library in a SwiftPM project, add the following line to the dependencies in your `Package.swift` file: | ||
|
||
```swift | ||
.package(url: "https://github.com/wiedem/enum-case-labeling", .upToNextMajor(from: "1.0.0")), | ||
``` | ||
|
||
Include `"EnumCaseLabeling"` as a dependency for your executable target: | ||
|
||
```swift | ||
dependencies: [ | ||
.product(name: "EnumCaseLabeling", package: "EnumCaseLabeling"), | ||
] | ||
``` | ||
|
||
## Usage | ||
|
||
Start by importing the module into your Swift code with `import EnumCaseLabeling`. | ||
|
||
### Extend Enumerations with the `CaseLabeled` Macro | ||
Apply the macro `CaseLabeled` to your enumeration: | ||
|
||
```swift | ||
@CaseLabeled | ||
enum MyEnum: Equatable { | ||
case intValue(Int) | ||
case stringValue(string: String) | ||
} | ||
``` | ||
|
||
The macro automatically declares a `CaseLabel` enumeration conforming to the `Equatable` protocol without associated values. | ||
A `caseLabel` property returns a value of `CaseLabel` for each case of the enumeration. | ||
|
||
### Using Case Labels | ||
Case labels of enumeration values can be used to identify values with an identical label, even if their associated values are not identical: | ||
```swift | ||
let value1: MyEnum = .intValue(1) | ||
let value2: MyEnum = .intValue(2) | ||
|
||
// value1 and value2 are not equal because their associated values are not equal ... | ||
print("value1 and value2 are equal: \(value1 == value2)") | ||
// ... but they share a common case label | ||
print("value1 and value2 have a common case label: \(value1.caseLabel == value2.caseLabel)") | ||
``` | ||
|
||
The `CaseLabeled` protocol also provides a convenience operator `~=` for the label comparison: | ||
```swift | ||
print("value1 and value2 have a common case label: \(value1 ~= value2)") | ||
``` | ||
|
||
Enumeration values can also be directly compared with case label values: | ||
```swift | ||
print("value1 is an 'intValue': \(value1 ~= .intValue)") | ||
``` | ||
|
||
This makes it possible, for example, to easily extend collections with methods that make use of the labels: | ||
|
||
```swift | ||
@CaseLabeled | ||
enum MyEnum: Hashable { | ||
case intValue(Int) | ||
case stringValue(string: String) | ||
} | ||
|
||
extension Set where Element: CaseLabeled { | ||
func remove(_ labeled: Element.CaseLabel) -> Self { | ||
filter { | ||
$0.caseLabel != labeled | ||
} | ||
} | ||
} | ||
|
||
let values: Set<MyEnum> = [ | ||
.stringValue(string: "Text1"), | ||
.intValue(1), | ||
.intValue(2), | ||
.stringValue(string: "Text2"), | ||
] | ||
|
||
// This removes all enumeration values with the `intValue` label. | ||
let filtered = values.remove(.intValue) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/// A type that provides a case label. | ||
/// | ||
/// Conformance to this protocol makes a case label available to other APIs that fulfill the [Equatable](https://developer.apple.com/documentation/swift/equatable) protocol. | ||
/// | ||
/// Case labels offer a way of grouping different values of a type and making them comparable. | ||
/// The protocol is primarily intended for use with enumerations having cases with associated types. | ||
/// | ||
/// The `~=` operator can be used to compare types implementing `CaseLabeled`; if the ``caseLabel-swift.property`` value is the same, the operator returns true: | ||
/// | ||
/// ```swift | ||
/// @CaseLabeled | ||
/// enum MyEnum { | ||
/// case `default`, simpleCase | ||
/// case intValue(Int) | ||
/// case stringValue(string: String?) | ||
/// } | ||
/// | ||
/// let value1: MyEnum = .intValue(1) | ||
/// let value2: MyEnum = .intValue(2) | ||
/// | ||
/// if value1 ~= value2 { | ||
/// print("Enum values '\(value1)' and '\(value2)' have a common case label") | ||
/// } | ||
/// ``` | ||
public protocol CaseLabeled { | ||
associatedtype CaseLabel: Equatable | ||
var caseLabel: CaseLabel { get } | ||
} | ||
|
||
public extension CaseLabeled { | ||
static func ~= (lhs: Self, rhs: Self) -> Bool { | ||
lhs.caseLabel == rhs.caseLabel | ||
} | ||
|
||
static func ~= (lhs: Self, rhs: CaseLabel) -> Bool { | ||
lhs.caseLabel == rhs | ||
} | ||
|
||
static func ~= (lhs: CaseLabel, rhs: Self) -> Bool { | ||
lhs == rhs.caseLabel | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/// A macro that implements labels for enum cases and conformance to the CaseLabeled protocol. | ||
/// | ||
/// This macro adds labels to enum cases and conforms the type to the ``CaseLabeled`` protocol. | ||
/// | ||
/// Case labels allow more convenient handling and comparison of enumeration cases with associated values. | ||
/// The following code shows how to add case labels to a custom enum type: | ||
/// | ||
/// ```swift | ||
/// @CaseLabeled | ||
/// enum MyEnum: Hashable, Sendable { | ||
/// case `default`, simpleCase | ||
/// case intValue(Int) | ||
/// case stringValue(string: String?) | ||
/// } | ||
/// ``` | ||
@attached(member, names: arbitrary) | ||
@attached(extension, conformances: CaseLabeled) | ||
public macro CaseLabeled() = #externalMacro(module: "EnumCaseLabelingMacros", type: "EnumCaseLabelingMacro") |
139 changes: 139 additions & 0 deletions
139
Sources/EnumCaseLabelingMacros/EnumCaseLabelingMacro.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct EnumCaseLabelingMacro: MemberMacro { | ||
public static func expansion( | ||
of _: AttributeSyntax, | ||
providingMembersOf declaration: some DeclGroupSyntax, | ||
in _: some MacroExpansionContext | ||
) throws -> [DeclSyntax] { | ||
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { | ||
return [] | ||
} | ||
|
||
let caseElements = enumDecl.memberBlock.members | ||
.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } | ||
.map { | ||
$0.elements.map { | ||
EnumCaseElementSyntax(name: $0.name) | ||
} | ||
} | ||
.flatMap { $0 } | ||
|
||
guard caseElements.isEmpty == false else { | ||
return [] | ||
} | ||
|
||
// Create the CaseLabel declaration. | ||
let enumCaseElementList = EnumCaseElementListSyntax.init { | ||
for element in caseElements { | ||
element | ||
} | ||
} | ||
let enumCase = EnumCaseDeclSyntax(elements: enumCaseElementList) | ||
|
||
let inheritanceTypeList = InheritedTypeListSyntax { | ||
InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("Hashable"))) | ||
InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("CaseIterable"))) | ||
InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("Sendable"))) | ||
} | ||
|
||
let labelEnumDecl = EnumDeclSyntax( | ||
name: .identifier("CaseLabel"), | ||
inheritanceClause: .init(inheritedTypes: inheritanceTypeList) | ||
) { | ||
enumCase | ||
} | ||
|
||
let labelCaseList = caseElements.map { enumCaseElement in | ||
SwitchCaseSyntax( | ||
label: .case(.init( | ||
caseItems: .init { | ||
.init(pattern: ExpressionPatternSyntax( | ||
expression: MemberAccessExprSyntax( | ||
declName: .init(baseName: enumCaseElement.name) | ||
) | ||
)) | ||
} | ||
)), | ||
statements: .init { | ||
MemberAccessExprSyntax( | ||
declName: .init( | ||
baseName: enumCaseElement.name | ||
) | ||
) | ||
} | ||
) | ||
} | ||
|
||
// Create the caseLabel var declaration. | ||
let labelVarDecl = VariableDeclSyntax( | ||
bindingSpecifier: .keyword(.var), | ||
bindings: .init { | ||
PatternBindingSyntax( | ||
pattern: IdentifierPatternSyntax(identifier: .identifier("caseLabel")), | ||
typeAnnotation: .init( | ||
type: IdentifierTypeSyntax( | ||
name: .identifier("CaseLabel") | ||
) | ||
), | ||
accessorBlock: AccessorBlockSyntax( | ||
accessors: .init( | ||
CodeBlockItemListSyntax { | ||
CodeBlockItemSyntax(item: .init(ExpressionStmtSyntax( | ||
expression: SwitchExprSyntax( | ||
subject: DeclReferenceExprSyntax(baseName: .keyword(.self)), | ||
cases: SwitchCaseListSyntax { | ||
for switchCase in labelCaseList { | ||
switchCase | ||
} | ||
} | ||
) | ||
))) | ||
} | ||
) | ||
) | ||
) | ||
} | ||
) | ||
|
||
return [ | ||
DeclSyntax(labelEnumDecl), | ||
DeclSyntax(labelVarDecl), | ||
] | ||
} | ||
} | ||
|
||
extension EnumCaseLabelingMacro: ExtensionMacro { | ||
public static func expansion( | ||
of _: AttributeSyntax, | ||
attachedTo _: some DeclGroupSyntax, | ||
providingExtensionsOf type: some TypeSyntaxProtocol, | ||
conformingTo protocols: [TypeSyntax], | ||
in _: some MacroExpansionContext | ||
) throws -> [ExtensionDeclSyntax] { | ||
guard protocols.isEmpty == false else { | ||
return [] | ||
} | ||
|
||
let labeledExtension: DeclSyntax = | ||
""" | ||
extension \(type.trimmed): \(raw: protocols.first!.trimmed) {} | ||
""" | ||
|
||
guard let extensionDecl = labeledExtension.as(ExtensionDeclSyntax.self) else { | ||
return [] | ||
} | ||
|
||
return [extensionDecl] | ||
} | ||
} | ||
|
||
@main | ||
struct EnumCaseLabelingPlugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
EnumCaseLabelingMacro.self, | ||
] | ||
} |
Oops, something went wrong.