diff --git a/.swiftpm/ThrowPublisherClient.xctestplan b/.swiftpm/ThrowPublisherClient.xctestplan
new file mode 100644
index 0000000..92940fc
--- /dev/null
+++ b/.swiftpm/ThrowPublisherClient.xctestplan
@@ -0,0 +1,37 @@
+{
+ "configurations" : [
+ {
+ "id" : "90C4A170-E408-4028-9F94-9180680D569E",
+ "name" : "Test Scheme Action",
+ "options" : {
+
+ }
+ }
+ ],
+ "defaultOptions" : {
+ "codeCoverage" : {
+ "targets" : [
+ {
+ "containerPath" : "container:",
+ "identifier" : "ThrowPublisherTests",
+ "name" : "ThrowPublisherTests"
+ }
+ ]
+ },
+ "targetForVariableExpansion" : {
+ "containerPath" : "container:",
+ "identifier" : "ThrowPublisherClient",
+ "name" : "ThrowPublisherClient"
+ }
+ },
+ "testTargets" : [
+ {
+ "target" : {
+ "containerPath" : "container:",
+ "identifier" : "ThrowPublisherTests",
+ "name" : "ThrowPublisherTests"
+ }
+ }
+ ],
+ "version" : 1
+}
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme
new file mode 100644
index 0000000..82ced7a
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher-Package.xcscheme
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme
new file mode 100644
index 0000000..d4ea2d3
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisher.xcscheme
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme
new file mode 100644
index 0000000..aa9b6c6
--- /dev/null
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/ThrowPublisherClient.xcscheme
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/ThrowPublisherClient/main.swift b/Sources/ThrowPublisherClient/main.swift
index e96fbc3..0616e22 100644
--- a/Sources/ThrowPublisherClient/main.swift
+++ b/Sources/ThrowPublisherClient/main.swift
@@ -34,6 +34,39 @@ struct MyStruct {
func doSomething(arg: T, arg2: P) throws -> String where T: Equatable {
"Something"
}
+
+ @ThrowPublisher
+ static func doSomething(arg: String) throws -> String {
+ "Something"
+ }
+
+ @ThrowPublisher
+ var something: String {
+ get throws {
+ "Something"
+ }
+ }
+
+ @ThrowPublisher
+ var somethingWithVoid: Void {
+ get throws {
+ print("Something")
+ }
+ }
+
+ @ThrowPublisher
+ var somethingOptional: String? {
+ get throws {
+ "Something"
+ }
+ }
+
+ @ThrowPublisher
+ static var somethingStatic: String? {
+ get throws {
+ "Something"
+ }
+ }
}
extension Array {
@@ -45,7 +78,19 @@ extension Array {
let myStruct = MyStruct()
-myStruct.doSomething_publisher()
+myStruct.something_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)
+
+myStruct.doSomething_publisher(arg: "Something")
.sink { completion in
switch completion {
case .finished:
diff --git a/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift b/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift
index 1795903..d7d16fa 100644
--- a/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift
+++ b/Sources/ThrowPublisherMacros/ThrowPublisherMacro.swift
@@ -28,94 +28,175 @@ enum ThrowPublisherError: Error, CustomStringConvertible {
}
public struct ThrowPublisherMacro: PeerMacro {
+ private static func expansionForFunction(functionDecl: FunctionDeclSyntax) throws -> [DeclSyntax] {
+ guard functionDecl.signature.effectSpecifiers?.asyncSpecifier == nil else {
+ throw ThrowPublisherError.asyncSpecifier
+ }
+ guard functionDecl.signature.effectSpecifiers?.throwsSpecifier != nil else {
+ throw ThrowPublisherError.noThrow
+ }
- 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 modifiers = functionDecl.modifiers.map {
+ $0.name.text
+ }.joined(separator: " ")
- let modifiers = functionDecl.modifiers.map {
- $0.name.text
- }.joined(separator: " ")
-
- 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 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"
+ let functionName = functionDecl.name.text
+ var newFunctionName = "\(functionName)_publisher"
- if let genericParameterClause = functionDecl.genericParameterClause {
- newFunctionName += "\(genericParameterClause)"
- }
+ if let genericParameterClause = functionDecl.genericParameterClause {
+ newFunctionName += "\(genericParameterClause)"
+ }
- var genericPart: String?
- if let genericWhereClause = functionDecl.genericWhereClause {
- genericPart = "where \(genericWhereClause.requirements)"
- }
+ var genericPart: String?
+ if let genericWhereClause = functionDecl.genericWhereClause {
+ genericPart = "where \(genericWhereClause.requirements)"
+ }
- 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)
- """
- }
+ 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: ", ")
- var returnPart = "AnyPublisher<\(returnType), Error>"
- if let genericPart {
- returnPart += " \(genericPart)"
- }
+ let resultString: String
+ if returnType == "Void" {
+ resultString = """
+ try \(functionName)(\(parametersWithCall))
+ return .success(())
+ """
+ } else {
+ resultString = """
+ let result = try \(functionName)(\(parametersWithCall))
+ return .success(result)
+ """
+ }
- var startOfFunction = "func"
- if !modifiers.isEmpty {
- startOfFunction = "\(modifiers) \(startOfFunction)"
- }
- let hop = """
- \(startOfFunction) \(newFunctionName)\(parameters)-> \(returnPart){
- func getResult() -> Result<\(returnType), Error> {
- do {
- \(resultString)
- } catch {
- return .failure(error)
- }
- }
- return getResult()
- .publisher
- .eraseToAnyPublisher()
- }
- """
+ var returnPart = "AnyPublisher<\(returnType), Error>"
+ if let genericPart {
+ returnPart += " \(genericPart)"
+ }
+
+ var startOfFunction = "func"
+ if !modifiers.isEmpty {
+ startOfFunction = "\(modifiers) \(startOfFunction)"
+ }
+ let hop = """
+ \(startOfFunction) \(newFunctionName)\(parameters)-> \(returnPart){
+ func getResult() -> Result<\(returnType), Error> {
+ do {
+ \(resultString)
+ } catch {
+ return .failure(error)
+ }
+ }
+ return getResult()
+ .publisher
+ .eraseToAnyPublisher()
+ }
+ """
+
+ return ["""
+ \(raw: hop)
+ """
+ ]
+ }
- return ["""
- \(raw: hop)
- """
- ]
+ private static func expansionForVariable(variableDecl: VariableDeclSyntax) throws -> [DeclSyntax] {
+ guard variableDecl.bindingSpecifier.tokenKind == .keyword(.var) else {
+ throw ThrowPublisherError.asyncSpecifier
+ }
+
+ guard variableDecl.bindings.count == 1 else { throw ThrowPublisherError.asyncSpecifier }
+ let firstBinding = variableDecl.bindings.first!
+
+ guard case .accessors(let accessors) = firstBinding.accessorBlock?.accessors else {
+ throw ThrowPublisherError.asyncSpecifier
+ }
+ if accessors.contains(where: { $0.effectSpecifiers?.asyncSpecifier != nil }) {
+ throw ThrowPublisherError.asyncSpecifier
+ }
+ guard accessors.contains(where: { $0.effectSpecifiers?.throwsSpecifier != nil }) else {
+ throw ThrowPublisherError.asyncSpecifier
+ }
+
+ let modifiers = variableDecl.modifiers.map {
+ $0.name.text
+ }.joined(separator: " ")
+
+ let returnType: String
+ if let name = firstBinding.typeAnnotation?.type.as(IdentifierTypeSyntax.self)?.name {
+ returnType = name.text
+ } else if let optionalTypeSyntax = firstBinding.typeAnnotation?.type.as(OptionalTypeSyntax.self),
+ let name = optionalTypeSyntax.wrappedType.as(IdentifierTypeSyntax.self)?.name {
+ returnType = "\(name.text)?"
+ } else {
+ returnType = "Void"
+ }
+
+ let variableName = "\(firstBinding.pattern)"
+ let newVaraibleName = "\(variableName)_publisher"
+
+ let resultString: String
+ if returnType == "Void" {
+ resultString = """
+ try \(variableName)
+ return .success(())
+ """
+ } else {
+ resultString = """
+ let result = try \(variableName)
+ return .success(result)
+ """
+ }
+
+ let returnPart = "AnyPublisher<\(returnType), Error>"
+
+ var startOfVariable = "var"
+ if !modifiers.isEmpty {
+ startOfVariable = "\(modifiers) \(startOfVariable)"
+ }
+
+ let hop = """
+ \(startOfVariable) \(newVaraibleName): \(returnPart){
+ func getResult() -> Result<\(returnType), Error> {
+ do {
+ \(resultString)
+ } catch {
+ return .failure(error)
+ }
+ }
+ return getResult()
+ .publisher
+ .eraseToAnyPublisher()
+ }
+ """
+
+ return ["""
+ \(raw: hop)
+ """
+ ]
+ }
+
+ public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
+ if let functionDecl = declaration.as(FunctionDeclSyntax.self) {
+ return try expansionForFunction(functionDecl: functionDecl)
+ } else if let variableDecl = declaration.as(VariableDeclSyntax.self) {
+ return try expansionForVariable(variableDecl: variableDecl)
+ }
+ throw ThrowPublisherError.notFunction
}
}
@main
diff --git a/Tests/ThrowPublisherTests/ThrowPublisherTests.swift b/Tests/ThrowPublisherTests/ThrowPublisherTests.swift
index b89ca51..995fae8 100644
--- a/Tests/ThrowPublisherTests/ThrowPublisherTests.swift
+++ b/Tests/ThrowPublisherTests/ThrowPublisherTests.swift
@@ -292,17 +292,21 @@ final class ThrowPublisherTests: XCTestCase {
#endif
}
- func testMacroWithNotFunction() throws {
+ func testMacroWithAsync() throws {
#if canImport(ThrowPublisherMacros)
assertMacroExpansion(
"""
@ThrowPublisher
- var someVariable: String
+ func someFunc() async throws {
+ print("hop")
+ }
""",
expandedSource: """
- var someVariable: String
+ func someFunc() async throws {
+ print("hop")
+ }
""",
- diagnostics: [.init(message: "Declaration must be function.", line: 1, column: 1)],
+ diagnostics: [.init(message: "ThrowPublisher doesn't support async specifier.", line: 1, column: 1)],
macros: testMacros
)
#else
@@ -310,21 +314,21 @@ final class ThrowPublisherTests: XCTestCase {
#endif
}
- func testMacroWithAsync() throws {
+ func testMacroWithNotThrowing() throws {
#if canImport(ThrowPublisherMacros)
assertMacroExpansion(
"""
@ThrowPublisher
- func someFunc() async throws {
+ func someFunc() {
print("hop")
}
""",
expandedSource: """
- func someFunc() async throws {
+ func someFunc() {
print("hop")
}
""",
- diagnostics: [.init(message: "ThrowPublisher doesn't support async specifier.", line: 1, column: 1)],
+ diagnostics: [.init(message: "Function doesn't throw.", line: 1, column: 1)],
macros: testMacros
)
#else
@@ -332,21 +336,155 @@ final class ThrowPublisherTests: XCTestCase {
#endif
}
- func testMacroWithNotThrowing() throws {
+ func testMacroVariable() throws {
#if canImport(ThrowPublisherMacros)
assertMacroExpansion(
"""
@ThrowPublisher
- func someFunc() {
- print("hop")
+ var someVariable: String {
+ get throws {
+ "Something"
+ }
}
""",
expandedSource: """
- func someFunc() {
- print("hop")
+ var someVariable: String {
+ get throws {
+ "Something"
+ }
+ }
+
+ var someVariable_publisher: AnyPublisher {
+ func getResult() -> Result {
+ do {
+ let result = try someVariable
+ return .success(result)
+ } catch {
+ return .failure(error)
+ }
+ }
+ return getResult()
+ .publisher
+ .eraseToAnyPublisher()
+ }
+ """,
+ macros: testMacros
+ )
+ #else
+ throw XCTSkip("macros are only supported when running tests for the host platform")
+ #endif
+ }
+
+ func testMacroVariableWithOptional() throws {
+ #if canImport(ThrowPublisherMacros)
+ assertMacroExpansion(
+ """
+ @ThrowPublisher
+ var someVariable: String? {
+ get throws {
+ "Something"
+ }
+ }
+ """,
+ expandedSource: """
+ var someVariable: String? {
+ get throws {
+ "Something"
+ }
+ }
+
+ var someVariable_publisher: AnyPublisher {
+ func getResult() -> Result {
+ do {
+ let result = try someVariable
+ return .success(result)
+ } catch {
+ return .failure(error)
+ }
+ }
+ return getResult()
+ .publisher
+ .eraseToAnyPublisher()
+ }
+ """,
+ macros: testMacros
+ )
+ #else
+ throw XCTSkip("macros are only supported when running tests for the host platform")
+ #endif
+ }
+
+ func testMacroVariableWithVoid() throws {
+ #if canImport(ThrowPublisherMacros)
+ assertMacroExpansion(
+ """
+ @ThrowPublisher
+ var someVariable: Void {
+ get throws {
+ "Something"
+ }
+ }
+ """,
+ expandedSource: """
+ var someVariable: Void {
+ get throws {
+ "Something"
+ }
+ }
+
+ var someVariable_publisher: AnyPublisher {
+ func getResult() -> Result {
+ do {
+ try someVariable
+ return .success(())
+ } catch {
+ return .failure(error)
+ }
+ }
+ return getResult()
+ .publisher
+ .eraseToAnyPublisher()
+ }
+ """,
+ macros: testMacros
+ )
+ #else
+ throw XCTSkip("macros are only supported when running tests for the host platform")
+ #endif
+ }
+
+ func testMacroVariableWithStatic() throws {
+ #if canImport(ThrowPublisherMacros)
+ assertMacroExpansion(
+ """
+ @ThrowPublisher
+ static var someVariable: String {
+ get throws {
+ "Something"
+ }
+ }
+ """,
+ expandedSource: """
+ static var someVariable: String {
+ get throws {
+ "Something"
+ }
+ }
+
+ static var someVariable_publisher: AnyPublisher {
+ func getResult() -> Result {
+ do {
+ let result = try someVariable
+ return .success(result)
+ } catch {
+ return .failure(error)
+ }
+ }
+ return getResult()
+ .publisher
+ .eraseToAnyPublisher()
}
""",
- diagnostics: [.init(message: "Function doesn't throw.", line: 1, column: 1)],
macros: testMacros
)
#else