Skip to content

Commit

Permalink
Extract out Codable Builder type
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Nov 26, 2023
1 parent 7f345be commit 55e96d5
Show file tree
Hide file tree
Showing 9 changed files with 146 additions and 38 deletions.
59 changes: 59 additions & 0 deletions Sources/SafeDICore/Builder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

public struct Builder: Codable, Equatable {

// MARK: Initialization

init(
typeName: String,
builtPropertyName: String,
builtType: String,
dependencies: [Dependency]
) {
builtProduct = Property(
label: builtPropertyName,
type: builtType
)
builder = Property(
label: Self.propertyNameSuffix(forProperty: builtPropertyName),
type: typeName
)
self.dependencies = dependencies
}

// MARK: Public

/// The injectable built product created by this builder.
public let builtProduct: Property
/// The injectable builder property that represents this builder.
public let builder: Property
/// This builder's dependencies.
public let dependencies: [Dependency]

// MARK: Private

/// The label suffix on all builder properties.
private static let propertyNameSuffix = "Builder"

private static func propertyNameSuffix(forProperty label: String) -> String {
label + Self.propertyNameSuffix
}
}
38 changes: 30 additions & 8 deletions Sources/SafeDICore/BuilderVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,29 +110,51 @@ public final class BuilderVisitor: SyntaxVisitor {
}

public override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
if node.name.text == DependenciesVisitor.decoratedStructName {
if
let builderMacro = node.attributes.builderMacro,
let propertyName = builderMacro.arguments?.firstArgumentString
{
builderTypeName = node.name.text
builtPropertyName = propertyName
return .visitChildren

} else if node.attributes.dependenciesMacro != nil {
didFindDependencies = true
dependenciesVisitor.walk(node)
return .skipChildren

} else {
return .skipChildren
}
return .skipChildren
}

// MARK: Public

public var dependencies: [Dependency] {
dependenciesVisitor.dependencies
}
public var builtType: String? {
dependenciesVisitor.builtType
public var builder: Builder? {
guard
let builtPropertyName,
let builtType = dependenciesVisitor.builtType,
let builderTypeName
else {
return nil
}
return Builder(
typeName: builderTypeName,
builtPropertyName: builtPropertyName,
builtType: builtType,
dependencies: dependenciesVisitor.dependencies
)
}

public private(set) var didFindDependencies = false
public private(set) var diagnostics = [Diagnostic]()

public static let macroName = "builder"
public static let decoratedStructName = "Builder"
public static let getDependenciesClosureName = "getDependencies"

// MARK: Private

private let dependenciesVisitor = DependenciesVisitor()
private var builderTypeName: String?
private var builtPropertyName: String?
}
2 changes: 1 addition & 1 deletion Sources/SafeDICore/DependenciesVisitor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ public final class DependenciesVisitor: SyntaxVisitor {
return .skipChildren
}

guard node.attributes.isDecoratedWithDependenciesMacro else {
guard node.attributes.dependenciesMacro != nil else {
var newAttributes = node.attributes
newAttributes.append(.attribute(
AttributeSyntax(
Expand Down
6 changes: 3 additions & 3 deletions Sources/SafeDICore/Errors/FixableBuilderError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ public enum FixableBuilderError: DiagnosticError {
case .missingDependencies:
return "Missing nested `@\(DependenciesVisitor.macroName) public struct \(DependenciesVisitor.decoratedStructName)` declaration"
case .unexpectedVariableDeclaration:
return "Found unexpected variable declaration in `\(BuilderVisitor.decoratedStructName)`"
return "Found unexpected variable declaration in `@\(BuilderVisitor.macroName)`"
case .unexpectedInitializer:
return "Found unexpected initializer in `\(BuilderVisitor.decoratedStructName)`"
return "Found unexpected initializer in `@\(BuilderVisitor.macroName)`"
case .unexpectedFuncationDeclaration:
return "Found unexpected function declaration in `\(BuilderVisitor.decoratedStructName)`"
return "Found unexpected function declaration in `@\(BuilderVisitor.macroName)`"
}
}

Expand Down
10 changes: 5 additions & 5 deletions Sources/SafeDICore/Extensions/ArrayExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ extension Array where Element == Dependency {
public var variantParameterList: FunctionParameterListSyntax {
FunctionParameterListSyntax(
filter { $0.source == .variant }
.map { "\(raw: $0.property.label): \(raw: $0.property.type)" }
.map { "\(raw: $0.property.asParameterDeclaration)" }
.transformUntilLast {
var functionPamameterSyntax = $0
functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present)
Expand All @@ -51,20 +51,20 @@ extension Array where Element == Dependency {

public var variantUnlabeledExpressionList: String {
filter { $0.isVariant }
.map { "\($0.property.label)" }
.map(\.property.label)
.joined(separator: ", ")
}

public var variantLabeledExpressionList: String {
filter { $0.isVariant }
.map { "\($0.property.label): \($0.property.label)" }
.map(\.property.asLabeledParameterExpression)
.joined(separator: ", ")
}

public var invariantParameterList: FunctionParameterListSyntax {
FunctionParameterListSyntax(
filter { $0.isInvariant }
.map { "\(raw: $0.property.label): \(raw: $0.property.type)" }
.map { "\(raw: $0.property.asParameterDeclaration)" }
.transformUntilLast {
var functionPamameterSyntax = $0
functionPamameterSyntax.trailingComma = TokenSyntax(.comma, presence: .present)
Expand All @@ -77,7 +77,7 @@ extension Array where Element == Dependency {
public var invariantAssignmentExpressionList: String {
"""
\(filter(\.isInvariant)
.map { "self.\($0.property.label) = \($0.property.label)" }
.map(\.property.asSelfAssignment)
.joined(separator: "\n"))
"""
}
Expand Down
23 changes: 20 additions & 3 deletions Sources/SafeDICore/Extensions/AttributeListSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,32 @@ import SwiftSyntax

extension AttributeListSyntax {

public var isDecoratedWithDependenciesMacro: Bool {
contains(where: { element in
var dependenciesMacro: AttributeSyntax? {
guard let attribute = first(where: { element in
switch element {
case let .attribute(attribute):
return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == DependenciesVisitor.macroName
case .ifConfigDecl:
return false
}
})
}) else {
return nil
}
return AttributeSyntax(attribute)
}

var builderMacro: AttributeSyntax? {
guard let attribute = first(where: { element in
switch element {
case let .attribute(attribute):
return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == BuilderVisitor.macroName
case .ifConfigDecl:
return false
}
}) else {
return nil
}
return AttributeSyntax(attribute)
}

public var attributedNodes: [(attribute: String, node: AttributeListSyntax.Element)] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import SwiftSyntax

extension AttributeSyntax.Arguments {
public var string: String? {
var firstArgumentString: String? {
switch self {
case let .argumentList(labeledExprListSyntax):
return labeledExprListSyntax
Expand Down
16 changes: 16 additions & 0 deletions Sources/SafeDICore/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,20 @@ public struct Property: Codable, Equatable {
public let label: String
/// The type to which the property conforms.
public let type: String

public var asPropertyDeclaration: String {
"let \(label): \(type)"
}

public var asParameterDeclaration: String {
"\(label): \(type)"
}

public var asLabeledParameterExpression: String {
"\(label): \(label)"
}

public var asSelfAssignment: String {
"self.\(label) = \(label)"
}
}
28 changes: 11 additions & 17 deletions Sources/SafeDIMacros/Macros/BuilderMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,11 @@ public struct BuilderMacro: MemberMacro {
}

let builderVisitor = BuilderVisitor()
builderVisitor.walk(structDelcaration.memberBlock)
builderVisitor.walk(structDelcaration)
for diagnostic in builderVisitor.diagnostics {
context.diagnose(diagnostic)
}

guard
let builderMacroArguments = node.arguments,
let builtPropertyName = builderMacroArguments.string
else {
// Builder macro is misconfigured. Compiler will highlight the issue – just fail to expand.
return []
}

guard builderVisitor.didFindDependencies else {
var membersWithDependencies = structDelcaration.memberBlock.members
membersWithDependencies.append(
Expand All @@ -79,15 +71,17 @@ public struct BuilderMacro: MemberMacro {
return []
}

let variantUnlabeledParameterList = builderVisitor.dependencies.variantUnlabeledParameterList
let variantParameterList = builderVisitor.dependencies.variantParameterList
let variantUnlabeledExpressionList = builderVisitor.dependencies.variantUnlabeledExpressionList
let variantLabeledExpressionList = builderVisitor.dependencies.variantLabeledExpressionList
guard let builtType = builderVisitor.builtType else {
guard let builder = builderVisitor.builder else {
// Builder macro is misconfigured. Compiler will highlight the issue – just fail to expand.
return []
}
let builtPropertyDescription = "let \(builtPropertyName): \(builtType)"
let builderPropertyDescription = "let \(builtPropertyName)\(BuilderVisitor.decoratedStructName): \(structDelcaration.name.text)"

let variantUnlabeledParameterList = builder.dependencies.variantUnlabeledParameterList
let variantParameterList = builder.dependencies.variantParameterList
let variantUnlabeledExpressionList = builder.dependencies.variantUnlabeledExpressionList
let variantLabeledExpressionList = builder.dependencies.variantLabeledExpressionList
let builtPropertyDescription = builder.builtProduct.asPropertyDeclaration
let builderPropertyDescription = builder.builder.asPropertyDeclaration
return [
"""
// Inject this builder as a dependency by adding `\(raw: builderPropertyDescription)` to your @\(raw: DependenciesVisitor.macroName) type
Expand All @@ -97,7 +91,7 @@ public struct BuilderMacro: MemberMacro {
""",
"""
// Inject this built product as a dependency by adding `\(raw: builtPropertyDescription)` to your @\(raw: DependenciesVisitor.macroName) type
public func build(\(variantParameterList)) -> \(raw: builtType) {
public func build(\(variantParameterList)) -> \(raw: builder.builtProduct.type) {
\(raw: BuilderVisitor.getDependenciesClosureName)(\(raw: variantUnlabeledExpressionList)).build(\(raw: variantLabeledExpressionList))
}
""",
Expand Down

0 comments on commit 55e96d5

Please sign in to comment.