Skip to content

Commit c9fb3c6

Browse files
authored
Introduce @CaseIterable (#2)
1 parent a177dc2 commit c9fb3c6

File tree

16 files changed

+1010
-118
lines changed

16 files changed

+1010
-118
lines changed

Package.swift

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import CompilerPluginSupport
44
import PackageDescription
55

66
let package = Package(
7-
name: "StaticMemberIterable",
7+
name: "swift-iterable-macros",
88
platforms: [
99
.iOS(.v13),
1010
.macOS(.v10_15),
@@ -13,17 +13,49 @@ let package = Package(
1313
.watchOS(.v6),
1414
],
1515
products: [
16+
.library(name: "IterableMacros", targets: ["IterableMacros"]),
1617
.library(name: "StaticMemberIterable", targets: ["StaticMemberIterable"]),
18+
.library(name: "CaseIterable", targets: ["CaseIterable"]),
1719
],
1820
targets: [
19-
.target(name: "StaticMemberIterable", dependencies: ["StaticMemberIterableMacro"]),
21+
.target(
22+
name: "IterableMacros",
23+
dependencies: [
24+
"StaticMemberIterable",
25+
"CaseIterable",
26+
],
27+
),
28+
29+
.target(
30+
name: "StaticMemberIterable",
31+
dependencies: [
32+
"IterableSupport",
33+
"StaticMemberIterableMacro",
34+
],
35+
),
36+
37+
.target(
38+
name: "CaseIterable",
39+
dependencies: [
40+
"IterableSupport",
41+
"CaseIterableMacro",
42+
],
43+
),
2044

2145
.macro(
2246
name: "StaticMemberIterableMacro",
2347
dependencies: [
2448
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
2549
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
26-
]
50+
],
51+
),
52+
53+
.macro(
54+
name: "CaseIterableMacro",
55+
dependencies: [
56+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
57+
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
58+
],
2759
),
2860

2961
.testTarget(
@@ -35,9 +67,23 @@ let package = Package(
3567
// For some reason, with Swift Syntax prebuilts enabled, we need to depend on SwiftCompilerPlugin here to work around error:
3668
// Compilation search paths unable to resolve module dependency: 'SwiftCompilerPlugin'
3769
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
38-
]
70+
],
3971
),
40-
]
72+
73+
.testTarget(
74+
name: "CaseIterableTests",
75+
dependencies: [
76+
"CaseIterable",
77+
"CaseIterableMacro",
78+
.product(name: "MacroTesting", package: "swift-macro-testing"),
79+
// For some reason, with Swift Syntax prebuilts enabled, we need to depend on SwiftCompilerPlugin here to work around error:
80+
// Compilation search paths unable to resolve module dependency: 'SwiftCompilerPlugin'
81+
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
82+
],
83+
),
84+
85+
.target(name: "IterableSupport"),
86+
],
4187
)
4288

4389
package.dependencies += [

README.md

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
# StaticMemberIterable
1+
# swift-iterable-macros
22

3-
StaticMemberIterable is a Swift macro that synthesizes collections describing every `static let` defined in a struct, enum, or class.
3+
swift-iterable-macros hosts Swift macros that generate iterable collections for your types:
4+
5+
- `@StaticMemberIterable` synthesizes collections describing every `static let` defined in a struct, enum, or class.
6+
- `@CaseIterable` mirrors Swift’s `CaseIterable` but keeps a case’s name, value, and presentation metadata.
47

58
This is handy for building fixtures, demo data, menus, or anywhere you want a single source of truth for a handful of well-known static members.
69

@@ -9,14 +12,21 @@ This is handy for building fixtures, demo data, menus, or anywhere you want a si
912
Add the dependency and product to your `Package.swift`:
1013

1114
```swift
12-
.package(url: "https://github.com/davdroman/StaticMemberIterable", from: "0.1.0"),
15+
.package(url: "https://github.com/davdroman/swift-iterable-macros", from: "0.2.0"),
16+
```
17+
18+
```swift
19+
.product(name: "IterableMacros", package: "swift-iterable-macros"),
1320
```
1421

22+
`IterableMacros` re-exports both modules. If you only need one macro, depend on it explicitly instead:
23+
1524
```swift
16-
.product(name: "StaticMemberIterable", package: "StaticMemberIterable"),
25+
.product(name: "StaticMemberIterable", package: "swift-iterable-macros"),
26+
.product(name: "CaseIterable", package: "swift-iterable-macros"),
1727
```
1828

19-
## Usage
29+
## Static members (`@StaticMemberIterable`)
2030

2131
```swift
2232
import StaticMemberIterable
@@ -29,7 +39,7 @@ enum ColorPalette {
2939
static let stardust: Color = Color(red: 0.68, green: 0.51, blue: 0.78)
3040
}
3141

32-
ColorPalette.allStaticMembers.map(\.value) // [.orange, .indigo, .purple] as [Color]
42+
ColorPalette.allStaticMembers.map(\.value) // [Color(red: 1.00, ...), ...]
3343
ColorPalette.allStaticMembers.map(\.title) // ["Sunrise", "Moonlight", "Stardust"]
3444
ColorPalette.allStaticMembers.map(\.keyPath) // [\ColorPalette.sunrise, ...] as [KeyPath<ColorPalette.Type, Color>]
3545
```
@@ -40,6 +50,7 @@ Each synthesized entry is a `StaticMember<Container, Value>`: an `Identifiable`
4050

4151
```swift
4252
ForEach(ColorPalette.allStaticMembers) { $color in
53+
let color = $color.value
4354
RoundedRectangle(cornerRadius: 12)
4455
.fill(color)
4556
.overlay(Text($color.title))
@@ -56,13 +67,36 @@ ForEach(ColorPalette.allStaticMembers) { $color in
5667

5768
Because it is a property wrapper, you can also project (`$member`) when you use it on your own properties, and `Identifiable` conformance makes it slot neatly into `ForEach`.
5869

70+
## Enum cases (`@CaseIterable`)
71+
72+
```swift
73+
import CaseIterable
74+
75+
@CaseIterable
76+
enum MenuSection {
77+
case breakfast
78+
case lunch
79+
case dinner
80+
}
81+
82+
ForEach(MenuSection.allCases) { $section in
83+
Text($section.title)
84+
.tag($section.id)
85+
}
86+
```
87+
88+
`@CaseIterable` produces an explicit `allCases: [CaseOf<Enum>]`. `CaseOf` is also a property wrapper, exposing the case name, a title-cased variant, the enum value, and a stable `id` derived from the name.
89+
5990
### Access control
6091

6192
Need public-facing lists? Pass the desired access modifier:
6293

6394
```swift
6495
@StaticMemberIterable(.public)
6596
struct Coffee { ... }
97+
98+
@CaseIterable(.public)
99+
enum MenuSection { ... }
66100
```
67101

68102
Supported modifiers:
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@attached(
2+
member,
3+
names: named(allCases), named(subscript(dynamicMember:))
4+
)
5+
public macro CaseIterable(
6+
_ access: CaseIterableAccess? = nil,
7+
) = #externalMacro(
8+
module: "CaseIterableMacro",
9+
type: "CaseIterableMacro",
10+
)
11+
12+
public enum CaseIterableAccess {
13+
case `public`
14+
case `internal`
15+
case `package`
16+
case `fileprivate`
17+
case `private`
18+
}

Sources/CaseIterable/CaseOf.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import IterableSupport
2+
3+
@propertyWrapper
4+
public struct CaseOf<Enum> {
5+
public let name: String
6+
7+
private let storage: Enum
8+
9+
public var wrappedValue: Enum { storage }
10+
public var projectedValue: CaseOf<Enum> { self }
11+
public var value: Enum { storage }
12+
public var title: String { name.memberIdentifierTitle() }
13+
14+
public init(name: String, value: Enum) {
15+
self.name = name
16+
self.storage = value
17+
}
18+
19+
public init(projectedValue: CaseOf<Enum>) {
20+
self.init(name: projectedValue.name, value: projectedValue.value)
21+
}
22+
}
23+
24+
extension CaseOf: Identifiable {
25+
public var id: String { name }
26+
}
27+
28+
extension CaseOf: @unchecked Sendable where Enum: Sendable {}

0 commit comments

Comments
 (0)