Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import CompilerPluginSupport
import PackageDescription

let package = Package(
name: "StaticMemberIterable",
name: "swift-iterable-macros",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
Expand All @@ -13,17 +13,49 @@ let package = Package(
.watchOS(.v6),
],
products: [
.library(name: "IterableMacros", targets: ["IterableMacros"]),
.library(name: "StaticMemberIterable", targets: ["StaticMemberIterable"]),
.library(name: "CaseIterable", targets: ["CaseIterable"]),
],
targets: [
.target(name: "StaticMemberIterable", dependencies: ["StaticMemberIterableMacro"]),
.target(
name: "IterableMacros",
dependencies: [
"StaticMemberIterable",
"CaseIterable",
],
),

.target(
name: "StaticMemberIterable",
dependencies: [
"IterableSupport",
"StaticMemberIterableMacro",
],
),

.target(
name: "CaseIterable",
dependencies: [
"IterableSupport",
"CaseIterableMacro",
],
),

.macro(
name: "StaticMemberIterableMacro",
dependencies: [
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
]
],
),

.macro(
name: "CaseIterableMacro",
dependencies: [
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
],
),

.testTarget(
Expand All @@ -35,9 +67,23 @@ let package = Package(
// For some reason, with Swift Syntax prebuilts enabled, we need to depend on SwiftCompilerPlugin here to work around error:
// Compilation search paths unable to resolve module dependency: 'SwiftCompilerPlugin'
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
],
),
]

.testTarget(
name: "CaseIterableTests",
dependencies: [
"CaseIterable",
"CaseIterableMacro",
.product(name: "MacroTesting", package: "swift-macro-testing"),
// For some reason, with Swift Syntax prebuilts enabled, we need to depend on SwiftCompilerPlugin here to work around error:
// Compilation search paths unable to resolve module dependency: 'SwiftCompilerPlugin'
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
],
),

.target(name: "IterableSupport"),
],
)

package.dependencies += [
Expand Down
46 changes: 40 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# StaticMemberIterable
# swift-iterable-macros

StaticMemberIterable is a Swift macro that synthesizes collections describing every `static let` defined in a struct, enum, or class.
swift-iterable-macros hosts Swift macros that generate iterable collections for your types:

- `@StaticMemberIterable` synthesizes collections describing every `static let` defined in a struct, enum, or class.
- `@CaseIterable` mirrors Swift’s `CaseIterable` but keeps a case’s name, value, and presentation metadata.

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.

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

```swift
.package(url: "https://github.com/davdroman/StaticMemberIterable", from: "0.1.0"),
.package(url: "https://github.com/davdroman/swift-iterable-macros", from: "0.2.0"),
```

```swift
.product(name: "IterableMacros", package: "swift-iterable-macros"),
```

`IterableMacros` re-exports both modules. If you only need one macro, depend on it explicitly instead:

```swift
.product(name: "StaticMemberIterable", package: "StaticMemberIterable"),
.product(name: "StaticMemberIterable", package: "swift-iterable-macros"),
.product(name: "CaseIterable", package: "swift-iterable-macros"),
```

## Usage
## Static members (`@StaticMemberIterable`)

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

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

```swift
ForEach(ColorPalette.allStaticMembers) { $color in
let color = $color.value
RoundedRectangle(cornerRadius: 12)
.fill(color)
.overlay(Text($color.title))
Expand All @@ -56,13 +67,36 @@ ForEach(ColorPalette.allStaticMembers) { $color in

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`.

## Enum cases (`@CaseIterable`)

```swift
import CaseIterable

@CaseIterable
enum MenuSection {
case breakfast
case lunch
case dinner
}

ForEach(MenuSection.allCases) { $section in
Text($section.title)
.tag($section.id)
}
```

`@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.

### Access control

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

```swift
@StaticMemberIterable(.public)
struct Coffee { ... }

@CaseIterable(.public)
enum MenuSection { ... }
```

Supported modifiers:
Expand Down
18 changes: 18 additions & 0 deletions Sources/CaseIterable/CaseIterable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@attached(
member,
names: named(allCases), named(subscript(dynamicMember:))
)
public macro CaseIterable(
_ access: CaseIterableAccess? = nil,
) = #externalMacro(
module: "CaseIterableMacro",
type: "CaseIterableMacro",
)

public enum CaseIterableAccess {
case `public`
case `internal`
case `package`
case `fileprivate`
case `private`
}
28 changes: 28 additions & 0 deletions Sources/CaseIterable/CaseOf.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import IterableSupport

@propertyWrapper
public struct CaseOf<Enum> {
public let name: String

private let storage: Enum

public var wrappedValue: Enum { storage }
public var projectedValue: CaseOf<Enum> { self }
public var value: Enum { storage }
public var title: String { name.memberIdentifierTitle() }

public init(name: String, value: Enum) {
self.name = name
self.storage = value
}

public init(projectedValue: CaseOf<Enum>) {
self.init(name: projectedValue.name, value: projectedValue.value)
}
}

extension CaseOf: Identifiable {
public var id: String { name }
}

extension CaseOf: @unchecked Sendable where Enum: Sendable {}
Loading
Loading