-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Proposal of using protocols in Injects
- Loading branch information
Showing
9 changed files
with
286 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
InjectGrailMacros/Sources/InjectGrailMacros/InjectGrailMacros.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// 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(member, names: named(`injector`), named(init(injector:))) | ||
public macro Needs<T>(noConstructor: Bool = false, noLet: Bool = false) = #externalMacro(module: "InjectGrailMacrosMacros", type: "NeedsMacro") | ||
|
||
@attached(member, names: named(`injector`), named(init(injector:))) | ||
public macro NeedsInjector() = #externalMacro(module: "InjectGrailMacrosMacros", type: "NeedsInjectorMacro") | ||
|
||
@attached(member) | ||
public macro Injects<each T>() = #externalMacro(module: "InjectGrailMacrosMacros", type: "InjectsMacro") |
48 changes: 48 additions & 0 deletions
48
InjectGrailMacros/Sources/InjectGrailMacrosClient/main.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import InjectGrailMacros | ||
|
||
protocol TestProtocol { | ||
} | ||
struct TestProtocolImpl: TestProtocol { | ||
|
||
} | ||
|
||
@Needs<TestProtocol> | ||
class Test { | ||
|
||
} | ||
|
||
@Needs<TestProtocol>(noConstructor: true) | ||
class TestNoConstructor { | ||
init(injector: TestProtocolImpl) { | ||
self.injector = injector | ||
} | ||
} | ||
|
||
|
||
@Needs<TestProtocol> | ||
class TestNoLet { | ||
|
||
} | ||
|
||
@Needs<TestProtocol> | ||
@Injects<TestNoConstructor, | ||
TestNoLet, | ||
TestNoLet, | ||
TestNoLet, | ||
TestNoLet, | ||
TestNoLet, | ||
TestNoLet, | ||
TestNoLet, | ||
TestNoLet,TestNoLet,TestNoLet,TestNoLet,TestNoLet> | ||
class TestNothing { | ||
let injector: TestProtocolImpl | ||
|
||
init(injector: TestProtocolImpl) { | ||
self.injector = injector | ||
} | ||
} | ||
|
||
let _ = Test(injector: TestProtocolImpl()) | ||
let _ = TestNoConstructor(injector: TestProtocolImpl()) | ||
let _ = TestNoLet(injector: TestProtocolImpl()) | ||
let _ = TestNothing(injector: TestProtocolImpl()) |
12 changes: 12 additions & 0 deletions
12
InjectGrailMacros/Sources/InjectGrailMacrosMacros/InjectGrailError.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// | ||
// File.swift | ||
// | ||
// | ||
// Created by Łukasz Kwoska on 15/11/2023. | ||
// | ||
|
||
import Foundation | ||
|
||
public struct InjectGrailError: Error { | ||
public let message: String | ||
} |
87 changes: 87 additions & 0 deletions
87
InjectGrailMacros/Sources/InjectGrailMacrosMacros/InjectGrailMacrosMacro.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacros | ||
|
||
public struct NeedsMacro: MemberMacro { | ||
public static func expansion( | ||
of node: AttributeSyntax, | ||
providingMembersOf declaration: some DeclGroupSyntax, | ||
in context: some MacroExpansionContext | ||
) throws -> [DeclSyntax] { | ||
guard declaration.as(ClassDeclSyntax.self) != nil else { | ||
throw InjectGrailError(message: "@Needs only works on class declaration") | ||
} | ||
|
||
guard | ||
let generics = node.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.first?.argument, | ||
let protocolType = generics.as(IdentifierTypeSyntax.self)?.name.text | ||
else { | ||
throw InjectGrailError(message: "@Needs requires Injector protocol") | ||
} | ||
|
||
let noLet = declaration.memberBlock.members.first(where: { $0.decl.as(VariableDeclSyntax.self)?.bindings.first(where: { "\($0.pattern)" == "injector" }) != nil}) != nil | ||
let noConstructor = declaration.memberBlock.members.first(where: { $0.decl.as(InitializerDeclSyntax.self) != nil})?.decl.as(InitializerDeclSyntax.self)!.signature.parameterClause.parameters.first?.firstName.text == "injector" | ||
|
||
|
||
return [ | ||
noLet ? nil : DeclSyntax(stringLiteral: "let injector: \(protocolType)Impl"), | ||
noConstructor ? nil : DeclSyntax(stringLiteral: "init(injector: \(protocolType)Impl){ self.injector = injector}") | ||
].compactMap({$0}) | ||
} | ||
} | ||
|
||
public struct NeedsInjectorMacro: MemberMacro { | ||
public static func expansion( | ||
of node: AttributeSyntax, | ||
providingMembersOf declaration: some DeclGroupSyntax, | ||
in context: some MacroExpansionContext | ||
) throws -> [DeclSyntax] { | ||
guard let classDeclaration = declaration.as(ClassDeclSyntax.self) else { | ||
throw InjectGrailError(message: "@NeedsInjector only works on class declaration") | ||
} | ||
|
||
let className = classDeclaration.name.text | ||
let injectorProtocolType = className.replacingOccurrences(of: "Impl", with: "Injector") | ||
|
||
let noLet = declaration.memberBlock.members.first(where: { $0.decl.as(VariableDeclSyntax.self)?.bindings.first(where: { "\($0.pattern)" == "injector" }) != nil}) != nil | ||
let noConstructor = declaration.memberBlock.members.first(where: { $0.decl.as(InitializerDeclSyntax.self) != nil})?.decl.as(InitializerDeclSyntax.self)!.signature.parameterClause.parameters.first?.firstName.text == "injector" | ||
|
||
return [ | ||
noLet ? nil : DeclSyntax(stringLiteral: "let injector: \(injectorProtocolType)Impl"), | ||
noConstructor ? nil : DeclSyntax(stringLiteral: "init(injector: \(injectorProtocolType)Impl){ self.injector = injector}") | ||
].compactMap({$0}) | ||
} | ||
} | ||
|
||
public struct InjectsMacro: MemberMacro { | ||
public static func expansion( | ||
of node: AttributeSyntax, | ||
providingMembersOf declaration: some DeclGroupSyntax, | ||
in context: some MacroExpansionContext | ||
) throws -> [DeclSyntax] { | ||
guard declaration.as(ClassDeclSyntax.self) != nil else { | ||
throw InjectGrailError(message: "@Injects only works on class declaration") | ||
} | ||
|
||
guard | ||
node.attributeName.as(IdentifierTypeSyntax.self)?.genericArgumentClause?.arguments.allSatisfy({ argument -> Bool in | ||
argument.as(GenericArgumentSyntax.self)?.argument.as(IdentifierTypeSyntax.self) != nil | ||
}) ?? false | ||
else { | ||
throw InjectGrailError(message: "@Injects requires Injected classes") | ||
} | ||
|
||
|
||
return [] | ||
} | ||
} | ||
|
||
@main | ||
struct InjectGrailMacrosPlugin: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
NeedsMacro.self, | ||
NeedsInjectorMacro.self, | ||
InjectsMacro.self | ||
] | ||
} |
64 changes: 64 additions & 0 deletions
64
InjectGrailMacros/Tests/InjectGrailMacrosTests/InjectGrailMacrosTests.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import SwiftSyntaxMacros | ||
import SwiftSyntaxMacrosTestSupport | ||
import XCTest | ||
|
||
// Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests. | ||
#if canImport(InjectGrailMacrosMacros) | ||
import InjectGrailMacrosMacros | ||
|
||
let testMacros: [String: Macro.Type] = [ | ||
"@Needs": NeedsMacro.self, | ||
] | ||
#endif | ||
|
||
final class InjectGrailMacrosTests: XCTestCase { | ||
func testMacro() throws { | ||
#if canImport(InjectGrailMacrosMacros) | ||
assertMacroExpansion( | ||
""" | ||
protocol TestProtocol { | ||
} | ||
@Needs<TestProtocol> | ||
class Test { | ||
} | ||
""", | ||
expandedSource: """ | ||
protocol TestProtocol { | ||
} | ||
@Needs<TestProtocol> | ||
class Test { | ||
let injector:TestProtocolImpl | ||
init(injector: TestProtocolImpl) { | ||
self.injector = injector | ||
} | ||
} | ||
""", | ||
macros: testMacros | ||
) | ||
#else | ||
throw XCTSkip("macros are only supported when running tests for the host platform") | ||
#endif | ||
} | ||
|
||
func testMacroWithStringLiteral() throws { | ||
#if canImport(InjectGrailMacrosMacros) | ||
assertMacroExpansion( | ||
#""" | ||
protocol TestProtocol { | ||
} | ||
@Needs(TestProtocol) | ||
protocol Test { | ||
} | ||
"""#, | ||
expandedSource: #""" | ||
"""#, | ||
macros: testMacros | ||
) | ||
#else | ||
throw XCTSkip("macros are only supported when running tests for the host platform") | ||
#endif | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" : "6ad4ea24b01559dde0773e3d091f1b9e36175036", | ||
"version" : "509.0.2" | ||
} | ||
} | ||
], | ||
"version" : 2 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,45 @@ | ||
// swift-tools-version:5.7 | ||
// swift-tools-version:5.9 | ||
|
||
import PackageDescription | ||
import CompilerPluginSupport | ||
|
||
let package = Package( | ||
name: "InjectGrail", | ||
platforms: [ | ||
.iOS(.v14) | ||
.iOS(.v14), | ||
.macOS(.v10_15) | ||
], | ||
products: [ | ||
.library(name: "InjectGrail", targets: ["InjectGrail"]) | ||
.library(name: "InjectGrail", targets: ["InjectGrail"]), | ||
.library(name: "InjectGrailMacros", targets: ["InjectGrailMacros"]), | ||
], | ||
dependencies: [ | ||
// Depend on the Swift 5.9 release of SwiftSyntax | ||
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), | ||
], | ||
targets: [ | ||
.target(name: "InjectGrail", path: "InjectGrail/Classes"), | ||
.target( | ||
name: "InjectGrailMacros", | ||
dependencies: ["InjectGrailMacrosMacros"], | ||
path: "InjectGrailMacros/Sources/InjectGrailMacros" | ||
), | ||
.macro( | ||
name: "InjectGrailMacrosMacros", | ||
dependencies: [ | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"), | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax") | ||
], | ||
path: "InjectGrailMacros/Sources/InjectGrailMacrosMacros" | ||
), | ||
.testTarget( | ||
name: "InjectGrailMacrosTests", | ||
dependencies: [ | ||
"InjectGrailMacrosMacros", | ||
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), | ||
], | ||
path: "InjectGrailMacros/Tests/InjectGrailMacrosTests" | ||
) | ||
], | ||
swiftLanguageVersions: [.v5] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters