Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CasePathIterable and CasePathReflectable protocols #173

Merged
merged 4 commits into from
Jul 12, 2024
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
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
stephencelis marked this conversation as resolved.
Show resolved Hide resolved
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> {
stephencelis marked this conversation as resolved.
Show resolved Hide resolved
/// 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