I'm trying to write an auto-assigning factory registering extension for test cases with the Factory library. This is where I am at:
import XCTest
import Factory
protocol Mock {
init()
}
protocol XCTestCaseProtocol: XCTestCase { }
extension XCTestCase: XCTestCaseProtocol { }
extension XCTestCaseProtocol {
/// Assigns the default initializer to the specified mock variable on the Test Case class, then registers it as the resolution for the specified factory.
@MainActor
func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
where T: Mock {
self[keyPath: mockKeyPath] = .init()
guard self[keyPath: mockKeyPath] is U else {
// Note: to my knowledge it's impossible to constrain T at compile-time to conform to U as of writing this function.
throw XCTestCaseCustomError.typeMismatch
}
Container.shared[keyPath: factoryKeyPath].register { @MainActor [unowned self] in
return self[keyPath: mockKeyPath] as! U
}
}
}
enum XCTestCaseCustomError: Error {
case typeMismatch
}
my problem is that I would much prefer to have a where
clause which constrains T to conformance to U, something like this:
func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
where T: Mock, T: U {
[...]
}
however this is seemingly impossible, because it throws this error:
Type 'T' constrained to non-protocol, non-class type 'U'
is there a way to make this work at compile time, rather than my current solution?
I'm trying to write an auto-assigning factory registering extension for test cases with the Factory library. This is where I am at:
import XCTest
import Factory
protocol Mock {
init()
}
protocol XCTestCaseProtocol: XCTestCase { }
extension XCTestCase: XCTestCaseProtocol { }
extension XCTestCaseProtocol {
/// Assigns the default initializer to the specified mock variable on the Test Case class, then registers it as the resolution for the specified factory.
@MainActor
func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
where T: Mock {
self[keyPath: mockKeyPath] = .init()
guard self[keyPath: mockKeyPath] is U else {
// Note: to my knowledge it's impossible to constrain T at compile-time to conform to U as of writing this function.
throw XCTestCaseCustomError.typeMismatch
}
Container.shared[keyPath: factoryKeyPath].register { @MainActor [unowned self] in
return self[keyPath: mockKeyPath] as! U
}
}
}
enum XCTestCaseCustomError: Error {
case typeMismatch
}
my problem is that I would much prefer to have a where
clause which constrains T to conformance to U, something like this:
func registerAndAssignMock<T, U>(_ mockKeyPath: ReferenceWritableKeyPath<Self, T>,
asFactory factoryKeyPath: KeyPath<Container, Factory<U>>) throws
where T: Mock, T: U {
[...]
}
however this is seemingly impossible, because it throws this error:
Type 'T' constrained to non-protocol, non-class type 'U'
is there a way to make this work at compile time, rather than my current solution?
Share Improve this question asked Jan 20 at 10:38 iSpain17iSpain17 3,0533 gold badges21 silver badges32 bronze badges 5 |2 Answers
Reset to default 0In Swift, you cannot constrain a type parameter with another type parameter:
This won't compile:
func foo<T, U>foo(...) where T: U
You might be inclined to constrain U to AnyObject
- but this does not work as well.
Same for associated types:
protocol P {
associatedtype Base /* : AnyObject */
associatedtype Derived: Base
}
neither does this work:
protocol BaseProtocol {
associatedtype Base: AnyObject
}
protocol DerivedProtocol: BaseProtocol {
associatedtype Derived: Base
}
neither does this work:
extension XCTestCaseProtocol {
func foo<T: Mock, F, U: AnyObject>(
mockKeyPath: ReferenceWritableKeyPath<Self, T>,
factoryKeyPath: KeyPath<Container, F>
) where F: FactoryModifying, F.T == U, T: U {
}
}
I would suggest, to change your design something along this:
protocol MyDependency { ... }
struct MyMock: MyDependency { ... }
struct MyRealThing: MyDependency { ... }
and then define the type of the factory:
Factory<any MyDependency>
Give up the idea, that you can have an additional constraint of your Mock to a certain base class, you don't need it anyway.
Then you can write:
extension XCTestCaseProtocol {
func foo<Mock: MyDependency>(
mockKeyPath: ReferenceWritableKeyPath<Self, Mock>,
factoryKeyPath: KeyPath<Container, Factory<any MyDependency>>
) {
}
}
Note: you only need to add any
as a prefix before the protocol name, when the protocol has associated types. However, for clarity, that this is an existential, I would suggest to add it always.
This might be overkill, but you can write a macro that inlines the body of registerAndAssignMock
into the use-site. The compiler can then check the types at the use-site.
// declaration:
@freestanding(expression)
public macro registerAndAssignMock<T: Mock, U, Root>(
_ mockKeyPath: ReferenceWritableKeyPath<Root, T>,
asFactory factoryKeyPath: KeyPath<Container, Factory<U>>
) = #externalMacro(module: "...", type: "RegisterMockMacro")
// implementation:
enum RegisterMockMacro: ExpressionMacro {
static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax {
let mockKeyPath = node.arguments.first?.expression
let factoryKeyPath = node.arguments.dropFirst().first?.expression
return """
{
self[keyPath: \(mockKeyPath)] = .init()
Container.shared[keyPath: \(factoryKeyPath)].register { @MainActor [unowned self] in
return self[keyPath: \(mockKeyPath)]
}
}()
"""
}
}
// example usage:
#registerAndAssignMock(\Self.foo, asFactory: \.barFactory)
// note that the root type cannot be inferred for the first argument, so you need to write "Self" explicitly.
The line return self[keyPath: \(mockKeyPath)]
is now at the use-site, so the compiler can easily tell whether the types are compatible or not.
Another design is a body macro, taking in pairs of mock key paths and factory key paths, and inserts them at the start of the function body.
// declaration:
@attached(body)
public macro Mocks<Root, each T: Mock, each U>(
_ mocks: repeat (ReferenceWritableKeyPath<Root, each T>, KeyPath<Container, Factory<each U>>)
) = #externalMacro(module: "...", type: "RegisterMockMacro2")
// implementation:
enum RegisterMockMacro2: BodyMacro {
static func expansion(of node: AttributeSyntax, providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext) throws -> [CodeBlockItemSyntax] {
guard let existingBody = declaration.body else { return [] }
guard case let .argumentList(args) = node.arguments else { return [] }
var results = [CodeBlockItemSyntax]()
for arg in args {
guard let tuple = arg.expression.as(TupleExprSyntax.self) else { continue }
let mockKeyPath = tuple.elements.first?.expression
let factoryKeyPath = tuple.elements.dropFirst().first?.expression
results.append("self[keyPath: \(mockKeyPath)] = .init()")
results.append("Container.shared[keyPath: \(factoryKeyPath)].register { @MainActor [unowned self] in return self[keyPath: \(mockKeyPath)] }")
}
results.append(contentsOf: existingBody.statements)
return results
}
}
// example usage:
@Mocks(
(\Self.foo, \.barFactory),
(\.anotherThing, \.anotherFactory),
(\.thirdThing, \.thirdFactory)
// and so on...
)
func testSomething() {
// ...
}
: U
is only valid if U is a class or protocol as the error message says but you have not defined any constraints for U as far as I can see so it could be any type. Maybe you need to implement a custom protocol that can be used to define the relationship between T and U – Joakim Danielson Commented Jan 20 at 11:35return self[keyPath: mockKeyPath]
would produce an error if the types are incompatible. – Sweeper Commented Jan 20 at 11:46Cat
mock object can be used to inject anAnimal
dependency. – Sweeper Commented Jan 20 at 21:27