Open ArseneSoulie opened 3 days ago
@ArseneSoulie Thanks for the issue!
I love this idea, but unfortunately a macro can only generate extensions for the type it was attached to.:
The generated extensions of the macro must only extend the type the macro is attached to.
So the @Mockable
macro on Refillable
is not allowed to generate an extension of MockVehicle
.
I have thought about this limitation a lot and would rather go for the direction of mock implementation classes inheriting from the parent protocol's mock implementation class.
This would require serious changes to the core implementation of the library, but I will give it a try shortly.
The generated extensions of the macro must only extend the type the macro is attached to.
Wow I did not know about that, thanks for the info !
mock implementation classes inheriting from the parent protocol's mock implementation class
That's another thing that I thought about but how would you go around to do it since multiple class inheritance is not supported ? In the case where a protocol inherits from more than one for example.
The solution i see would be to only allow for a single inheritance and tell a user to "flatten their inheritance"
from
protocol A: B, C {}
protocol B {}
protocol C {}
protocol A: B {}
protocol B: C {}
protocol C {}
Good catch! I think lifting this limitation for single inheritances would be a big win already.
I've thought up of another idea using delegates. It could solve our multiple inheritance problem without needing to change the way we use the macro by keeping the simple @Mockable
.
Tell me if this could work in your opinion (there might be some more macro limitations that I don't know of)
@Mockable
protocol Drivable {
func drive()
}
// Macro expansion
final class MockDrivable: Drivable { // <- implementation as usual
func drive() {}
}
// we'll use a delegate to use the implementation of other classes
protocol DelegatableDrivable: AnyObject {
var delegateDrivable: Drivable { get }
}
// Here we call the implementation of the delegate
extension Drivable where Self: DelegatableDrivable {
func drive() {
delegateDrivable.drive()
}
}
Same thing for refillable
@Mockable
protocol Refillable {
func refill()
}
// Macro expansion
final class MockRefillable: Refillable {
func refill() {}
}
protocol DelegatableRefillable: AnyObject {
var delegateRefillable: Refillable { get }
}
extension Refillable where Self: DelegatableRefillable {
func refill() {
delegateRefillable.refill()
}
}
@Mockable
protocol Vehicle: Drivable, Refillable {
var numberOfWheels: Int { get }
}
//Macro expansion
final class MockVehicle: Drivable, Refillable, DelegatableDrivable, DelegatableRefillable, Vehicle {
var numberOfWheels { } // <- implementation as usual
var delegateRefillable: Refillable = MockRefillable()
var delegateDrivable: Drivable = MockDrivable()
}
// + same thing as the other w/ delegate etc.
let mockVehicle = MockVehicle()
// here we have now access to all the implementation methods without ever having to know them in the file
mockVehicle.numberOfWheels
mockVehicle.refill()
mockVehicle.driver()
This seems like a good workaround.
Not sure if the where Self: DelegatableRefillable
constraint is allowed in generated extensions, I need to check this.
This would in theory solve the conformance to parent protocols, but a challenge that still makes this seem impossible (same with the class inheritance idea) is that we still can't generate:
refill()
) inside the builder struct declarations without having this information at the time of macro expansion.
Without this you won't be able to write:
verify(mockVehicle)
.refill().called(.atLeastOnce)
That's right, I thought about it after having the last idea in the hopes that a solution exists 😅 I'll try some experiments with macros to see what's possible and what's not
I would like to have your thoughts on a potential evolution to make protocol inheritance possible with swift macros for mock generation. This is intended as a discussion topic rather than a formal issue.
Let's say that I have the following protocols
Let's add a new parameter to the
@Mockable
macro in order to know at the file level the other protocols who inherit from it. This takes the shape of a variadic list of protocolTaking back to our example it would look like this
This is possible because Swift allows declaring protocol conformance at the type declaration and implementing it in an extension.
We could include a docstring specifying that in the case of inherited protocols, conformance to the sub-protocol must be declared at the file level with
@Mockable(inheritedBy:<NAME OF THE PROTOCOL>)
Pros: It works 🎉
Cons: It requires to write the name of the inherited protocols at the parent protocol level which may feel a bit counterintuitive