Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
rocxteady committed Oct 24, 2023
0 parents commit 9809ce6
Show file tree
Hide file tree
Showing 9 changed files with 598 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .gitignore
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/contents.xcworkspacedata
.netrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
14 changes: 14 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "ffa3cd6fc2aa62adbedd31d3efaf7c0d86a9f029",
"version" : "509.0.1"
}
}
],
"version" : 2
}
52 changes: 52 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import CompilerPluginSupport

let package = Package(
name: "ThrowPublisher",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "ThrowPublisher",
targets: ["ThrowPublisher"]
),
.executable(
name: "ThrowPublisherClient",
targets: ["ThrowPublisherClient"]
),
],
dependencies: [
// Depend on the Swift 5.9 release of SwiftSyntax
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
// Macro implementation that performs the source transformation of a macro.
.macro(
name: "ThrowPublisherMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),

// Library that exposes a macro as part of its API, which is used in client programs.
.target(name: "ThrowPublisher", dependencies: ["ThrowPublisherMacros"]),

// A client of the library, which is able to use the macro in its own code.
.executableTarget(name: "ThrowPublisherClient", dependencies: ["ThrowPublisher"]),

// A test target used to develop the macro implementation.
.testTarget(
name: "ThrowPublisherTests",
dependencies: [
"ThrowPublisherMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# ThrowPublisher
11 changes: 11 additions & 0 deletions Sources/ThrowPublisher/ThrowPublisher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book

/// A macro that produces both a value and a string containing the
/// source code that generated the value. For example,
///
/// #stringify(x + y)
///
/// produces a tuple `(x + y, "x + y")`.
@attached(peer, names: arbitrary)
public macro ThrowPublisher() = #externalMacro(module: "ThrowPublisherMacros", type: "ThrowPublisherMacro")
52 changes: 52 additions & 0 deletions Sources/ThrowPublisherClient/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import ThrowPublisher
import Combine
import Foundation

var cancellables = Set<AnyCancellable>()

struct MyStruct {
@ThrowPublisher
func doSomething(arg: String) throws -> String {
"Something"
}

@ThrowPublisher
func doSomething(arg: (String) -> String) throws -> String {
arg("Something")
}

@ThrowPublisher
func doSomething(_ arg: String) throws -> String {
"Something"
}

@ThrowPublisher
func doSomething() throws -> Void {
print("Something")
}

@ThrowPublisher
func doSomething(arg: String?) throws -> String? {
nil
}

@ThrowPublisher
func doSomething<T>(arg: T) throws -> String where T: Equatable {
"Something"
}
}

let myStruct = MyStruct()

myStruct.doSomething_publisher()
.sink { completion in
switch completion {
case .finished:
print("finished")
case .failure(let error):
print(error.localizedDescription)
}
} receiveValue: { value in
print(value)
}.store(in: &cancellables)

130 changes: 130 additions & 0 deletions Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import Foundation

enum ThrowPublisherError: Error, CustomStringConvertible {
case notFunction
case wildcard
case identifierType
case asyncSpecifier
case noThrow
case unsupportedWhereClause

var description: String {
switch self {
case .notFunction:
return "Declaration must be function."
case .identifierType:
return "Parameter type must be identifier type."
case .wildcard:
return "Could not get the name of wildcard(_) parameter."
case .asyncSpecifier:
return "ThrowPublisher doesn't support async specifier."
case .unsupportedWhereClause:
return "Unsupported where clause."
case .noThrow:
return "Function doesn't throw."
}
}
}

public struct ThrowPublisherMacro: PeerMacro {

public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let functionDecl = declaration.as(FunctionDeclSyntax.self) else {
throw ThrowPublisherError.notFunction
}
guard functionDecl.signature.effectSpecifiers?.asyncSpecifier == nil else {
throw ThrowPublisherError.asyncSpecifier
}
guard functionDecl.signature.effectSpecifiers?.throwsSpecifier != nil else {
throw ThrowPublisherError.noThrow
}

let returnType: String
if let name = functionDecl.signature.returnClause?.type.as(IdentifierTypeSyntax.self)?.name {
returnType = name.text
} else if let optionalTypeSyntax = functionDecl.signature.returnClause?.type.as(OptionalTypeSyntax.self),
let name = optionalTypeSyntax.wrappedType.as(IdentifierTypeSyntax.self)?.name {
returnType = "\(name.text)?"
} else {
returnType = "Void"
}

let functionName = functionDecl.name.text
var newFunctionName = "\(functionName)_publisher"

if let genericParameterClause = functionDecl.genericParameterClause {
let generics = genericParameterClause.parameters.map {
$0.name.text
}.joined(separator: ", ")
newFunctionName += "<\(generics)>"
}

var genericPart: String?
if let genericWhereClause = functionDecl.genericWhereClause {
let generics = try genericWhereClause.requirements.map {
guard let conformanceRequirementSyntax = $0.requirement.as(ConformanceRequirementSyntax.self),
let leftType = conformanceRequirementSyntax.leftType.as(IdentifierTypeSyntax.self),
let rightType = conformanceRequirementSyntax.rightType.as(IdentifierTypeSyntax.self)else { throw ThrowPublisherError.unsupportedWhereClause }
return "\(leftType.name.text)\(conformanceRequirementSyntax.colon.text) \(rightType.name.text)"
}.joined(separator: ", ")
genericPart = "where \(generics)"
}

let parameters = "\(functionDecl.signature.parameterClause)"
let parametersWithCall = try functionDecl.signature.parameterClause.parameters.map {
if $0.firstName.text == "_" {
guard let secondName = $0.secondName else { throw ThrowPublisherError.wildcard }
return secondName.text
}
return "\($0.firstName.text): \($0.firstName.text)"
}.joined(separator: ", ")

let resultString: String
if returnType == "Void" {
resultString = """
try \(functionName)(\(parametersWithCall))
return .success(())
"""
} else {
resultString = """
let result = try \(functionName)(\(parametersWithCall))
return .success(result)
"""
}

var returnPart = "AnyPublisher<\(returnType), Error>"
if let genericPart {
returnPart += " \(genericPart)"
}

let hop = """
func \(newFunctionName)\(parameters)-> \(returnPart) {
func getResult() -> Result<\(returnType), Error> {
do {
\(resultString)
} catch {
return .failure(error)
}
}
return getResult()
.publisher
.eraseToAnyPublisher()
}
"""

return ["""
\(raw: hop)
"""
]
}
}
@main
struct ThrowPublisherPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
ThrowPublisherMacro.self,
]
}
Loading

0 comments on commit 9809ce6

Please sign in to comment.