Skip to content

Commit

Permalink
Add CasePathIterable and CasePathReflectable protocols (#173)
Browse files Browse the repository at this point in the history
* Add `CasePathIterable` and `CasePathReflectable` protocols

While the macro recently introduced iteration (#159) and "reflection"
(#158), there's no way to abstract over it. Adding it directly to the
`CasePathable` protocol would be potentially source breaking, so instead
we can introduce some new protocols.

* wip

* wip

* wip
  • Loading branch information
stephencelis authored Jul 12, 2024
1 parent 57580dd commit b9ad266
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 110 deletions.
17 changes: 17 additions & 0 deletions Sources/CasePaths/CasePathIterable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// A type that provides a collection of all of its case paths.
///
/// The ``CasePathable()`` macro automatically generates a conformance to this protocol.
///
/// You can iterate over ``CasePathable/allCasePaths`` to get access to each individual case path:
///
/// ```swift
/// @CasePathable enum Field {
/// case title(String)
/// case body(String
/// case isLive
/// }
///
/// Array(Field.allCasePaths) // [\.title, \.body, \.isLive]
/// ```
public protocol CasePathIterable: CasePathable
where AllCasePaths: Sequence, AllCasePaths.Element == PartialCaseKeyPath<Self> {}
27 changes: 27 additions & 0 deletions Sources/CasePaths/CasePathReflectable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// A type that can reflect a case path from a given case.
///
/// The ``CasePathable()`` macro automatically generates a conformance to this protocol on the
/// enum's ``CasePathable/AllCasePaths`` type.
///
/// You can look up an enum's case path by passing it to ``subscript(root:)``:
///
/// ```swift
/// @CasePathable
/// enum Field {
/// case title(String)
/// case body(String)
/// case isLive
/// }
///
/// Field.allCasePaths[.title("Hello, Blob!")] // \.title
/// ```
public protocol CasePathReflectable<Root> {
/// The enum type that can be reflected.
associatedtype Root: CasePathable

/// Returns the case key path for a given root value.
///
/// - Parameter root: An root value.
/// - Returns: A case path to the root value.
subscript(root: Root) -> PartialCaseKeyPath<Root> { get }
}
5 changes: 5 additions & 0 deletions Sources/CasePaths/Documentation.docc/CasePathable.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
- ``subscript(dynamicMember:)-7ik0u``
- ``subscript(dynamicMember:)-7sz4x``

### Iteration and reflection

- ``CasePathIterable``
- ``CasePathReflectable``

### Manual conformances

- ``AllCasePaths``
Expand Down
2 changes: 1 addition & 1 deletion Sources/CasePaths/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/// \UserAction.Cases.home // CasePath<UserAction, HomeAction>
/// \UserAction.Cases.settings // CasePath<UserAction, SettingsAction>
/// ```
@attached(extension, conformances: CasePathable)
@attached(extension, conformances: CasePathable, CasePathIterable)
@attached(member, names: named(AllCasePaths), named(allCasePaths))
public macro CasePathable() =
#externalMacro(
Expand Down
5 changes: 2 additions & 3 deletions Sources/CasePaths/Never+CasePathable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
extension Never: CasePathable {
public struct AllCasePaths: Sendable {
/// Returns the case key path for a given root value.
extension Never: CasePathable, CasePathIterable {
public struct AllCasePaths: CasePathReflectable, Sendable {
public subscript(root: Never) -> PartialCaseKeyPath<Never> {
\.never
}
Expand Down
5 changes: 2 additions & 3 deletions Sources/CasePaths/Optional+CasePathable.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
extension Optional: CasePathable {
extension Optional: CasePathable, CasePathIterable {
@dynamicMemberLookup
public struct AllCasePaths: Sendable {
/// Returns the case key path for a given root value.
public struct AllCasePaths: CasePathReflectable, Sendable {
public subscript(root: Optional) -> PartialCaseKeyPath<Optional> {
switch root {
case .none: return \.none
Expand Down
5 changes: 2 additions & 3 deletions Sources/CasePaths/Result+CasePathable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
extension Result: CasePathable {
public struct AllCasePaths: Sendable {
/// Returns the case key path for a given root value.
extension Result: CasePathable, CasePathIterable {
public struct AllCasePaths: CasePathReflectable, Sendable {
public subscript(root: Result) -> PartialCaseKeyPath<Result> {
switch root {
case .success: return \.success
Expand Down
62 changes: 40 additions & 22 deletions Sources/CasePathsMacros/CasePathableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import SwiftSyntaxMacros

public struct CasePathableMacro {
static let moduleName = "CasePaths"
static let conformanceName = "CasePathable"
static var qualifiedConformanceName: String { "\(Self.moduleName).\(Self.conformanceName)" }
static var conformanceNames: [String] { [Self.conformanceName, Self.qualifiedConformanceName] }
static let casePathTypeName = "AnyCasePath"
static var qualifiedCasePathTypeName: String { "\(Self.moduleName).\(Self.casePathTypeName)" }
static var qualifiedCaseTypeName: String { "\(Self.moduleName).Case" }
}

extension CasePathableMacro: ExtensionMacro {
Expand All @@ -29,18 +24,28 @@ extension CasePathableMacro: ExtensionMacro {
// TODO: Diagnostic?
return []
}
if let inheritanceClause = enumDecl.inheritanceClause,
inheritanceClause.inheritedTypes.contains(
where: { Self.conformanceNames.contains($0.type.trimmedDescription) }
)
{
return []
var conformances: [String] = []
if let inheritanceClause = enumDecl.inheritanceClause {
for type in ["CasePathable", "CasePathIterable"] {
if !inheritanceClause.inheritedTypes.contains(where: {
[type, type.qualified].contains($0.type.trimmedDescription)
}) {
conformances.append("\(moduleName).\(type)")
}
}
} else {
conformances = ["CasePathable", "CasePathIterable"].qualified
}
let ext: DeclSyntax =
"""
\(declaration.attributes.availability)extension \(type.trimmed): \(raw: Self.qualifiedConformanceName) {}
"""
return [ext.cast(ExtensionDeclSyntax.self)]
guard !conformances.isEmpty else { return [] }
return [
DeclSyntax(
"""
\(declaration.attributes.availability)extension \(type.trimmed): \
\(raw: conformances.joined(separator: ", ")) {}
"""
)
.cast(ExtensionDeclSyntax.self)
]
}
}

Expand Down Expand Up @@ -95,13 +100,14 @@ extension CasePathableMacro: MemberMacro {

return [
"""
public struct AllCasePaths: Sendable, Sequence {
public subscript(root: \(enumName)) -> PartialCaseKeyPath<\(enumName)> {
public struct AllCasePaths: CasePaths.CasePathReflectable, Sendable, Sequence {
public subscript(root: \(enumName)) -> CasePaths.PartialCaseKeyPath<\(enumName)> {
\(raw: rootSubscriptCases.map { "\($0.description)\n" }.joined())\(raw: subscriptReturn)
}
\(raw: casePaths.map(\.description).joined(separator: "\n"))
public func makeIterator() -> IndexingIterator<[PartialCaseKeyPath<\(enumName)>]> {
\(raw: allCases.isEmpty ? "let" : "var") allCasePaths: [PartialCaseKeyPath<\(enumName)>] = []\
public func makeIterator() -> IndexingIterator<[CasePaths.PartialCaseKeyPath<\(enumName)>]> {
\(raw: allCases.isEmpty ? "let" : "var") allCasePaths: \
[CasePaths.PartialCaseKeyPath<\(enumName)>] = []\
\(raw: allCases.map { "\n\($0.description)" }.joined())
return allCasePaths.makeIterator()
}
Expand Down Expand Up @@ -200,8 +206,8 @@ extension CasePathableMacro: MemberMacro {
.trimmingSuffix(while: { $0.isWhitespace && !$0.isNewline })
return """
\(raw: leadingTrivia)public var \(caseName): \
\(raw: qualifiedCasePathTypeName)<\(enumName), \(raw: associatedValueName)> {
\(raw: qualifiedCasePathTypeName)<\(enumName), \(raw: associatedValueName)>(
\(raw: casePathTypeName.qualified)<\(enumName), \(raw: associatedValueName)> {
\(raw: casePathTypeName.qualified)<\(enumName), \(raw: associatedValueName)>(
embed: { \(enumName).\(caseName)\(raw: embedNames) },
extract: {
guard case\(raw: hasPayload ? " let" : "").\(caseName)\(raw: bindingNames) = $0 else { \
Expand Down Expand Up @@ -472,6 +478,18 @@ final class SelfRewriter: SyntaxRewriter {
}
}

extension [String] {
fileprivate var qualified: [String] {
map(\.qualified)
}
}

extension String {
fileprivate var qualified: String {
"\(CasePathableMacro.moduleName).\(self)"
}
}

extension StringProtocol {
@inline(__always)
func trimmingSuffix(while condition: (Element) throws -> Bool) rethrows -> Self.SubSequence {
Expand Down
Loading

0 comments on commit b9ad266

Please sign in to comment.