Skip to content

Commit f97ee6b

Browse files
committed
Enable forwarding multiple properties
1 parent 4c83c05 commit f97ee6b

File tree

11 files changed

+603
-127
lines changed

11 files changed

+603
-127
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,9 @@ public struct ParentView: View {
192192

193193
Property declarations within `@Instantiable` types decorated with [`@Forwarded`](Sources/SafeDI/PropertyDecoration/Forwarded.swift) represent dependencies that come from the runtime, e.g. user input or backend-delivered content. Like an `@Instantiated`-decorated property, a `@Forwarded`-decorated property is available to be `@Received` by objects instantiated further down the dependency tree.
194194

195-
A `@Forwarded` property is forwarded into the SafeDI dependency tree by a [`ForwardingInstantiator`](#forwardinginstantiator)’s `instantiate(_ argument: ArgumentToForward) -> InstantiableType` function that creates an instance of the property’s enclosing type. `@Instantiable` types with a `@Forwarded`-decorated property can _only_ be instantiated utilizing a `ForwardingInstantiator`.
195+
A `@Forwarded` property is forwarded into the SafeDI dependency tree by a [`ForwardingInstantiator`](#forwardinginstantiator)’s `instantiate(_ arguments: ArgumentsToForward) -> InstantiableType` function that creates an instance of the property’s enclosing type. `@Instantiable` types with a `@Forwarded`-decorated property can _only_ be instantiated utilizing a `ForwardingInstantiator`.
196196

197-
A `ForwardingInstantiator`‘s single-argument `instantiate` function necessitates that an `@Instantiable` type may have at most one `@Forwarded`-decorated property. If you need to forward multiple properties, create a new container type that stores both properties and forward the container type. Forwarded property types do not need to be decorated with the `@Instantiable` macro.
197+
Forwarded property types do not need to be decorated with the `@Instantiable` macro.
198198

199199
### @Received
200200

Sources/SafeDI/DelayedInstantiation/ForwardingInstantiator.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,21 @@
2727
/// - SeeAlso: `Instantiator`
2828
/// - Note: This class is the sole means for instantiating an `@Instantiable` type with a `@Forwarded`
2929
/// property within the SafeDI framework.
30-
public final class ForwardingInstantiator<ArgumentToForward, InstantiableType> {
30+
public final class ForwardingInstantiator<ArgumentsToForward, InstantiableType> {
3131
/// Initializes a new forwarding instantiator with the provided instantiation closure.
3232
///
3333
/// - Parameter instantiator: A closure that takes `ArgumentsToForward` and returns an instance of `InstantiableType`.
34-
public init(_ instantiator: @escaping (ArgumentToForward) -> InstantiableType) {
34+
public init(_ instantiator: @escaping (ArgumentsToForward) -> InstantiableType) {
3535
self.instantiator = instantiator
3636
}
3737

3838
/// Instantiates and returns a new instance of the `@Instantiable` type, using the provided arguments.
3939
///
4040
/// - Parameter arguments: Arguments required for instantiation.
4141
/// - Returns: An `InstantiableType` instance.
42-
public func instantiate(_ argument: ArgumentToForward) -> InstantiableType {
43-
instantiator(argument)
42+
public func instantiate(_ arguments: ArgumentsToForward) -> InstantiableType {
43+
instantiator(arguments)
4444
}
4545

46-
private let instantiator: (ArgumentToForward) -> InstantiableType
46+
private let instantiator: (ArgumentsToForward) -> InstantiableType
4747
}

Sources/SafeDICore/Extensions/ArrayExtensions.swift renamed to Sources/SafeDICore/Extensions/CollectionExtensions.swift

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,37 @@
2121
import SwiftSyntax
2222
import SwiftSyntaxBuilder
2323

24-
extension Array where Element == Dependency {
25-
26-
var removingDuplicateInitializerArguments: Self {
27-
var alreadySeenInitializerArgument = Set<Property>()
28-
return filter {
29-
if alreadySeenInitializerArgument.contains($0.property) {
30-
return false
31-
} else {
32-
alreadySeenInitializerArgument.insert($0.property)
33-
return true
24+
extension Collection where Element == Dependency {
25+
26+
var initializerFunctionParameters: [FunctionParameterSyntax] {
27+
map(\.property)
28+
.initializerFunctionParameters
29+
}
30+
}
31+
32+
extension Collection where Element == Property {
33+
34+
public var asTuple: TupleTypeSyntax {
35+
let tupleElements = sorted()
36+
.map(\.asTupleElement)
37+
.transformUntilLast {
38+
var node = $0
39+
node.trailingComma = .commaToken(trailingTrivia: .space)
40+
return node
3441
}
42+
var tuple = TupleTypeSyntax(elements: TupleTypeElementListSyntax())
43+
for element in tupleElements {
44+
tuple.elements.append(element)
3545
}
46+
return tuple
47+
}
48+
49+
var asTupleTypeDescription: TypeDescription {
50+
TypeSyntax(asTuple).typeDescription
3651
}
3752

3853
var initializerFunctionParameters: [FunctionParameterSyntax] {
39-
removingDuplicateInitializerArguments
40-
.map { $0.property.asFunctionParamter }
54+
map { $0.asFunctionParamter }
4155
.transformUntilLast {
4256
var node = $0
4357
node.trailingComma = .commaToken(trailingTrivia: .space)
@@ -46,12 +60,12 @@ extension Array where Element == Dependency {
4660
}
4761
}
4862

49-
extension Array {
63+
extension Collection {
5064
fileprivate func transformUntilLast(_ transform: (Element) throws -> Element) rethrows -> [Element] {
51-
var arrayToTransform = self
65+
var arrayToTransform = Array(self)
5266
guard let lastItem = arrayToTransform.popLast() else {
5367
// Array is empty.
54-
return self
68+
return []
5569
}
5670
return try arrayToTransform.map { try transform($0) } + [lastItem]
5771
}

Sources/SafeDICore/Generators/ScopeGenerator.swift

Lines changed: 40 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,21 @@ actor ScopeGenerator {
3333
scopeData = .property(
3434
instantiable: instantiable,
3535
property: property,
36-
forwardedProperty: instantiable
37-
.dependencies
36+
forwardedProperties: Set(
37+
instantiable
38+
.dependencies
3839
// Instantiated properties will self-resolve.
39-
.filter { $0.source == .forwarded }
40-
.map(\.property)
41-
// Our @Instantiable macro enforces that we have at most one forwarded property.
42-
.first
40+
.filter { $0.source == .forwarded }
41+
.map(\.property)
42+
)
4343
)
4444
} else {
4545
scopeData = .root(instantiable: instantiable)
4646
}
4747
self.property = property
4848
self.receivedProperties = receivedProperties
4949
self.propertiesToGenerate = propertiesToGenerate
50-
forwardedProperty = scopeData.forwardedProperty
50+
forwardedProperties = scopeData.forwardedProperties
5151
propertiesMadeAvailableByChildren = Set(
5252
instantiable
5353
.dependencies
@@ -78,7 +78,7 @@ actor ScopeGenerator {
7878
scopeData = .alias(property: property, fulfillingProperty: fulfillingProperty)
7979
requiredReceivedProperties = [fulfillingProperty]
8080
propertiesToGenerate = []
81-
forwardedProperty = nil
81+
forwardedProperties = []
8282
propertiesMadeAvailableByChildren = []
8383
self.receivedProperties = receivedProperties
8484
self.property = property
@@ -111,7 +111,7 @@ actor ScopeGenerator {
111111
case let .property(
112112
instantiable,
113113
property,
114-
forwardedProperty
114+
forwardedProperties
115115
):
116116
let argumentList = try instantiable.generateArgumentList()
117117
let concreteTypeName = instantiable.concreteInstantiableType.asSource
@@ -127,17 +127,31 @@ actor ScopeGenerator {
127127
let propertyDeclaration: String
128128
let leadingConcreteTypeName: String
129129
let closureArguments: String
130-
if let forwardedProperty {
131-
guard property.generics.first == forwardedProperty.typeDescription else {
130+
if forwardedProperties.isEmpty {
131+
closureArguments = ""
132+
} else {
133+
if let firstForwardedProperty = forwardedProperties.first,
134+
let forwardedArgument = property.generics.first,
135+
!(
136+
// The forwarded argument is the same type as our only `@Forwarded` property.
137+
(forwardedProperties.count == 1 && forwardedArgument == firstForwardedProperty.typeDescription)
138+
// The forwarded argument is the same as `InstantiableTypeName.ForwardedArguments`.
139+
|| forwardedArgument == .nested(name: "ForwardedArguments", parentType: instantiable.concreteInstantiableType)
140+
// The forwarded argument is the same as the tuple we generated for `InstantiableTypeName.ForwardedArguments`.
141+
|| forwardedArgument == forwardedProperties.asTupleTypeDescription
142+
)
143+
{
132144
throw GenerationError.forwardingInstantiatorGenericDoesNotMatch(
133145
property: property,
134-
expectedType: forwardedProperty.typeDescription,
135146
instantiable: instantiable
136147
)
137148
}
138-
closureArguments = " \(forwardedProperty.label) in"
139-
} else {
140-
closureArguments = ""
149+
150+
let forwardedArgumentList = forwardedProperties
151+
.sorted()
152+
.map(\.label)
153+
.joined(separator: ", ")
154+
closureArguments = " \(forwardedArgumentList) in"
141155
}
142156
switch property.propertyType {
143157
case .instantiator, .forwardingInstantiator:
@@ -193,19 +207,19 @@ actor ScopeGenerator {
193207
case property(
194208
instantiable: Instantiable,
195209
property: Property,
196-
forwardedProperty: Property?
210+
forwardedProperties: Set<Property>
197211
)
198212
case alias(
199213
property: Property,
200214
fulfillingProperty: Property
201215
)
202216

203-
var forwardedProperty: Property? {
217+
var forwardedProperties: Set<Property> {
204218
switch self {
205-
case let .property(_, _, forwardedProperty):
206-
return forwardedProperty
219+
case let .property(_, _, forwardedProperties):
220+
return forwardedProperties
207221
case .root, .alias:
208-
return nil
222+
return []
209223
}
210224
}
211225
}
@@ -216,7 +230,7 @@ actor ScopeGenerator {
216230
private let receivedProperties: Set<Property>
217231
private let propertiesToGenerate: [ScopeGenerator]
218232
private let property: Property?
219-
private let forwardedProperty: Property?
233+
private let forwardedProperties: Set<Property>
220234

221235
private var resolvedProperties = Set<Property>()
222236
private var generateCodeTask: Task<String, Error>?
@@ -264,25 +278,25 @@ actor ScopeGenerator {
264278
.requiredReceivedProperties
265279
.contains(where: {
266280
!isPropertyResolved($0)
267-
&& propertyToGenerate.forwardedProperty != $0
281+
&& !propertyToGenerate.forwardedProperties.contains($0)
268282
})
269283
}
270284

271285
private func isPropertyResolved(_ property: Property) -> Bool {
272286
resolvedProperties.contains(property)
273287
|| receivedProperties.contains(property)
274-
|| forwardedProperty == property
288+
|| forwardedProperties.contains(property)
275289
}
276290

277291
// MARK: GenerationError
278292

279293
private enum GenerationError: Error, CustomStringConvertible {
280-
case forwardingInstantiatorGenericDoesNotMatch(property: Property, expectedType: TypeDescription, instantiable: Instantiable)
294+
case forwardingInstantiatorGenericDoesNotMatch(property: Property, instantiable: Instantiable)
281295

282296
var description: String {
283297
switch self {
284-
case let .forwardingInstantiatorGenericDoesNotMatch(property, expectedType, instantiable):
285-
"Property `\(property.asSource)` on \(instantiable.concreteInstantiableType.asSource) incorrectly configured. Property should instead be of type `\(Dependency.forwardingInstantiatorType)<\(expectedType.asSource), \(property.typeDescription.asInstantiatedType.asSource)>`. First generic argument must match type of @\(Dependency.Source.forwarded.rawValue) property."
298+
case let .forwardingInstantiatorGenericDoesNotMatch(property, instantiable):
299+
"Property `\(property.asSource)` on \(instantiable.concreteInstantiableType.asSource) incorrectly configured. Property should instead be of type `\(Dependency.forwardingInstantiatorType)<\(instantiable.concreteInstantiableType.asSource).ForwardedArguments, \(property.typeDescription.asInstantiatedType.asSource)>`."
286300
}
287301
}
288302
}

Sources/SafeDICore/Models/Initializer.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,9 @@ public struct Initializer: Codable, Hashable {
112112

113113
let dependencyAndArgumentBinding = try createDependencyAndArgumentBinding(given: dependencies)
114114

115-
let dependenciesWithDuplicateInitializerArgumentsRemoved = dependencies.removingDuplicateInitializerArguments
116115
let initializerFulfulledDependencies = Set(dependencyAndArgumentBinding.map(\.dependency))
117-
let missingArguments = Set(dependenciesWithDuplicateInitializerArgumentsRemoved).subtracting(initializerFulfulledDependencies)
118-
116+
let missingArguments = Set(dependencies).subtracting(initializerFulfulledDependencies)
117+
119118
guard missingArguments.isEmpty else {
120119
throw GenerationError.missingArguments(missingArguments.map(\.property.asSource))
121120
}

Sources/SafeDICore/Models/Property.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
4949

5050
// MARK: Internal
5151

52+
/// The property represented as source code.
5253
var asSource: String {
5354
"\(label): \(typeDescription.asSource)"
5455
}
@@ -61,6 +62,14 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
6162
)
6263
}
6364

65+
var asTupleElement: TupleTypeElementSyntax {
66+
TupleTypeElementSyntax(
67+
firstName: .identifier(label),
68+
colon: .colonToken(),
69+
type: IdentifierTypeSyntax(name: .identifier(typeDescription.asSource))
70+
)
71+
}
72+
6473
var propertyType: PropertyType {
6574
switch typeDescription {
6675
case let .simple(name, _):

Sources/SafeDICore/Models/TypeDescription.swift

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
4444
indirect case array(element: TypeDescription)
4545
/// A dictionary. e.g. [Int: String]
4646
indirect case dictionary(key: TypeDescription, value: TypeDescription)
47-
/// A tuple. e.g. (Int, String)
48-
indirect case tuple([TypeDescription])
47+
/// A tuple. e.g. (Int, string: String)
48+
indirect case tuple([TupleElement])
4949
/// A closure. e.g. (Int, Double) throws -> String
5050
indirect case closure(arguments: [TypeDescription], isAsync: Bool, doesThrow: Bool, returnType: TypeDescription)
5151
/// A type that can't be represented by the above cases.
@@ -117,7 +117,15 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
117117
case let .dictionary(key, value):
118118
return "Dictionary<\(key.asSource), \(value.asSource)>"
119119
case let .tuple(types):
120-
return "(\(types.map { $0.asSource }.joined(separator: ", ")))"
120+
return """
121+
(\(types.map {
122+
if let label = $0.label {
123+
"\(label): \($0.typeDescription.asSource)"
124+
} else {
125+
$0.typeDescription.asSource
126+
}
127+
}.joined(separator: ", ")))
128+
"""
121129
case let .closure(arguments, isAsync, doesThrow, returnType):
122130
return "(\(arguments.map { $0.asSource }.joined(separator: ", ")))\([isAsync ? " async" : "", doesThrow ? " throws" : ""].filter { !$0.isEmpty }.joined()) -> \(returnType.asSource)"
123131
case let .unknown(text):
@@ -129,6 +137,11 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
129137
lhs.asSource < rhs.asSource
130138
}
131139

140+
public struct TupleElement: Codable, Hashable, Sendable {
141+
public let label: String?
142+
public let typeDescription: TypeDescription
143+
}
144+
132145
var isOptional: Bool {
133146
switch self {
134147
case .any,
@@ -179,9 +192,9 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
179192
// This is a type that is lazily instantiated.
180193
// The first generic is the built type.
181194
return builtType
182-
} else if name == Dependency.forwardingInstantiatorType, let builtType = generics.last {
195+
} else if name == Dependency.forwardingInstantiatorType, let builtType = generics.dropFirst().first {
183196
// This is a type that is lazily instantiated with forwarded arguments.
184-
// The last generic is the built type.
197+
// The second generic is the built type.
185198
return builtType
186199
} else {
187200
return self
@@ -264,8 +277,13 @@ extension TypeSyntax {
264277
key: typeIdentifier.key.typeDescription,
265278
value: typeIdentifier.value.typeDescription)
266279

267-
} else if let typeIdentifiers = TupleTypeSyntax(self) {
268-
return .tuple(typeIdentifiers.elements.map { $0.type.typeDescription })
280+
} else if let typeIdentifier = TupleTypeSyntax(self) {
281+
return .tuple(typeIdentifier.elements.map {
282+
TypeDescription.TupleElement(
283+
label: $0.secondName?.text ?? $0.firstName?.text,
284+
typeDescription: $0.type.typeDescription
285+
)
286+
})
269287

270288
} else if ClassRestrictionTypeSyntax(self) != nil {
271289
// A class restriction is the same as requiring inheriting from AnyObject:
@@ -341,17 +359,22 @@ extension ExprSyntax {
341359
return .unknown(text: trimmedDescription)
342360
}
343361
} else if let tupleExpr = TupleExprSyntax(self) {
344-
let tupleTypes = tupleExpr.elements.map(\.expression.typeDescription)
345-
if tupleTypes.count == 1 {
362+
let tupleElements = tupleExpr.elements
363+
if tupleElements.count == 1 {
346364
// Single-element tuple types must be unwrapped.
347365
// Certain types can not be in a Any.Type list without being wrapped
348366
// in a tuple. We care only about the underlying types in this case.
349367
// A @Instantiable that fulfills an addition type `(some Collection).self`
350368
// should be unwrapped as `some Collection` to enable the @Instantiable
351369
// to fulfill `some Collection`.
352-
return tupleTypes[0]
370+
return tupleElements.lazy.map(\.expression)[0].typeDescription
353371
} else {
354-
return .tuple(tupleTypes)
372+
return .tuple(tupleElements.map {
373+
TypeDescription.TupleElement(
374+
label: $0.label?.text,
375+
typeDescription: $0.expression.typeDescription
376+
)
377+
})
355378
}
356379
} else if let sequenceExpr = SequenceExprSyntax(self) {
357380
if sequenceExpr.elements.contains(where: { BinaryOperatorExprSyntax($0) != nil }) {

0 commit comments

Comments
 (0)