Skip to content

Commit

Permalink
Add InjectableInit Macro (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
jrosen081 authored Jan 15, 2025
1 parent ce3bb94 commit 7f5405d
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 1 deletion.
4 changes: 4 additions & 0 deletions Sources/WhoopDIKit/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ public macro Injectable() = #externalMacro(module: "WhoopDIKitMacros", type: "In
/// The `@InjectableName` macro is used as a marker for the `@Injectable` protocol to add a `WhoopDI.inject(name)` in the inject method
@attached(peer)
public macro InjectableName(name: String) = #externalMacro(module: "WhoopDIKitMacros", type: "InjectableNameMacro")

/// The `@InjectableInit` macro is used as a marker for the `@Injectable` protocol to use as the init for the static `inject` function
@attached(peer)
public macro InjectableInit() = #externalMacro(module: "WhoopDIKitMacros", type: "InjectableInitMacro")
13 changes: 13 additions & 0 deletions Sources/WhoopDIKitMacros/InjectableInitMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxMacroExpansion

struct InjectableInitMacro: PeerMacro {
static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] {
guard declaration.kind == .initializerDecl else {
throw MacroExpansionErrorMessage("@InjectableInit can only be applied to an initializer")
}

return []
}
}
29 changes: 29 additions & 0 deletions Sources/WhoopDIKitMacros/InjectableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,35 @@ struct InjectableMacro: ExtensionMacro, MemberMacro {
throw MacroExpansionErrorMessage("@Injectable needs to be declared on a concrete type, not a protocol")
}

let allInjectableInits = declaration.allInjectableInits

if allInjectableInits.isEmpty {
return try createInitializerAndInject(declaration: declaration)
} else if allInjectableInits.count > 1 {
throw MacroExpansionErrorMessage("Only one initializer with the `@InjectableInit` macro is allowed")
} else {
let initValue = allInjectableInits[0]
return try createInject(from: initValue, declaration: declaration)
}
}

private static func createInject(from initValue: InitializerDeclSyntax, declaration: some DeclGroupSyntax) throws -> [DeclSyntax] {
let allArgs = initValue.signature.parameterClause.parameters.map { parameter in
"\(parameter.firstName.text == "_" ? "" : "\(parameter.firstName.text): ")container.inject()"
}.joined(separator: ", ")

let accessLevel = self.accessLevel(declaration: declaration) ?? "internal"

return [
"""
\(raw: accessLevel) static func inject(container: Container) -> Self {
Self.init(\(raw: allArgs))
}
"""
]
}

private static func createInitializerAndInject(declaration: some DeclGroupSyntax) throws -> [DeclSyntax] {
let allVariables = declaration.allMemberVariables

// Create the initializer args in the form `name: type = default`
Expand Down
3 changes: 2 additions & 1 deletion Sources/WhoopDIKitMacros/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import SwiftSyntaxMacros
struct WhoopDIKitPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
InjectableMacro.self,
InjectableNameMacro.self
InjectableNameMacro.self,
InjectableInitMacro.self
]
}
18 changes: 18 additions & 0 deletions Sources/WhoopDIKitMacros/Support/VariableDeclaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ extension DeclGroupSyntax {
}
}

var allInjectableInits: [InitializerDeclSyntax] {
self.memberBlock.members.compactMap { member in
if let initSyntax = member.decl.as(InitializerDeclSyntax.self),
initSyntax.attributes.contains(where: { element in
switch element {
case .attribute(let syntax):
return syntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "InjectableInit"
default:
return false
}
}) {
return initSyntax
} else {
return nil
}
}
}

private func injectableName(variableSyntax: VariableDeclSyntax) -> String? {
variableSyntax.attributes.compactMap { (attribute) -> String? in
switch attribute {
Expand Down
32 changes: 32 additions & 0 deletions Tests/WhoopDIKitTests/Injectable/InjectableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ final class InjectableTests: XCTestCase {
macros: ["Injectable": InjectableMacro.self, "InjectableName": InjectableNameMacro.self])
}

func testBasicInjectWithInjectableInit() {
assertMacroExpansion(
"""
@Injectable struct TestThing {
let bestThing: Int
@InjectableInit
internal init(notReal: Int, _ extraArg: String) {
self.bestThing = notReal
}
}
""",

expandedSource:
"""
struct TestThing {
let bestThing: Int
internal init(notReal: Int, _ extraArg: String) {
self.bestThing = notReal
}
internal static func inject(container: Container) -> Self {
Self.init(notReal: container.inject(), container.inject())
}
}
extension TestThing : Injectable {
}
""",
macros: ["Injectable": InjectableMacro.self, "InjectableName": InjectableNameMacro.self, "InjectableInit": InjectableInitMacro.self])
}

func testInjectWithSpecifiers() {
assertMacroExpansion(
"""
Expand Down

0 comments on commit 7f5405d

Please sign in to comment.