Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sourcery stencil for generic function using associatedType #1387

Open
LucasVanDongen opened this issue Dec 3, 2024 · 0 comments
Open

Sourcery stencil for generic function using associatedType #1387

LucasVanDongen opened this issue Dec 3, 2024 · 0 comments

Comments

@LucasVanDongen
Copy link

LucasVanDongen commented Dec 3, 2024

Swift Version

swift-driver version: 1.90.11.1 Apple Swift version 5.10 (swiftlang-5.10.0.13 clang-1500.3.9.4)
Target: arm64-apple-macosx14.0

Problem

I'm building a generic networking stack and it's working really well. However I want to mock the API and I am a bit stuck generating a Mock for it through Sourcery. I did some stencil modifications before, for example to generate @published / Publisher pairs automatically, so I'm not unfamiliar with this, but at the moment I'm stuck.

While it's not hard to mock the code below manually, I would like to understand the problem a bit better and I like things to work in a standardized way.

First my protocol and model:

// sourcery: AutoMockable
public protocol NetworkHandling {
    @discardableResult
    func handle<Request: NetworkRequest>(request: Request) async throws -> Request.ResponseData
}

// sourcery: AutoMockable
public protocol NetworkRequest { // This one generates fine!
    associatedtype RequestData: Encodable = EmptyData
    associatedtype ResponseData: Decodable = EmptyData

    var path: String { get }
    var needsToken: Bool { get }
    var contentType: ContentType { get }
    var method: HTTPMethod { get }
    var body: RequestData { get }
    var queryParameters: [String: String]? { get }
    var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy { get }
    var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { get }
    var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy { get }
    var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy { get }
}

The relevant part of my stencil:

{% macro methodName method %}func {{ method.shortName}}({%- for param in method.parameters %}{% if param.argumentLabel == nil %}_ {% if not param.name == "" %}{{ param.name }}{% else %}arg{{ param.index }}{% endif %}{%elif param.argumentLabel == param.name%}{{ param.name }}{%else%}{{ param.argumentLabel }} {{ param.name }}{% endif %}: {% if param.typeName.isClosure and param.typeName.closure.parameters.count > 1 %}({% endif %}{% call existentialParameterTypeName param.typeName param.isVariadic %}{% if not forloop.last %}, {% endif %}{% endfor -%}){% endmacro %}

The relevant part of AutoMockable.generated (reformatted for readability with a comment by me):

public class NetworkHandlingMock: NetworkHandling {
    public init() {}

    //MARK: - handle<Request: NetworkRequest>

    public var handleRequestNetworkRequestRequestRequestRequestResponseDataThrowableError: (any Error)?
    public var handleRequestNetworkRequestRequestRequestRequestResponseDataCallsCount = 0
    public var handleRequestNetworkRequestRequestRequestRequestResponseDataCalled: Bool {
        return handleRequestNetworkRequestRequestRequestRequestResponseDataCallsCount > 0
    }
    public var handleRequestNetworkRequestRequestRequestRequestResponseDataReceivedRequest: (Request)?
    public var handleRequestNetworkRequestRequestRequestRequestResponseDataReceivedInvocations: [(Request)] = []
    public var handleRequestNetworkRequestRequestRequestRequestResponseDataReturnValue: Request.ResponseData!
    // The next four declarations fail because `Request` is unknown
    public var handleRequestNetworkRequestRequestRequestRequestResponseDataClosure: ((Request) async throws -> Request.ResponseData)?

    @discardableResult
    public func handle<Request: NetworkRequest>(request: Request) async throws -> Request.ResponseData {
        handleRequestNetworkRequestRequestRequestRequestResponseDataCallsCount += 1
        handleRequestNetworkRequestRequestRequestRequestResponseDataReceivedRequest = request
        handleRequestNetworkRequestRequestRequestRequestResponseDataReceivedInvocations.append(request)
        if let error = handleRequestNetworkRequestRequestRequestRequestResponseDataThrowableError {
            throw error
        }
        if let handleRequestNetworkRequestRequestRequestRequestResponseDataClosure = handleRequestNetworkRequestRequestRequestRequestResponseDataClosure {
            return try await handleRequestNetworkRequestRequestRequestRequestResponseDataClosure(request)
        } else {
            return handleRequestNetworkRequestRequestRequestRequestResponseDataReturnValue
        }
    }
}

I guess the issue is that the types for the vars for the closure etcetera are typed towards the function, but the function itself is generic, so we don't know what kind of response a given request wants.

Manually I came to this solution:

class NetworkHandlingMock: NetworkHandling {
    private var presetReturnValues: [String: Any] = [:]
    private var presetErrors: [String: Error] = [:]
    private(set) var receivedRequests: [Any] = []

    @discardableResult
    func handle<Request: NetworkRequest>(request: Request) async throws -> Request.ResponseData {
        let key = String(describing: Request.self)
        receivedRequests.append(request)

        if let error = presetErrors[key] {
            throw error
        }

        guard let returnValue = presetReturnValues[key] as? Request.ResponseData else {
            fatalError("No preset return value provided for \(Request.self)")
        }

        return returnValue
    }

    func setReturnValue<Request: NetworkRequest>(_ returnValue: Request.ResponseData, for requestType: Request.Type) {
        let key = String(describing: Request.self)
        presetReturnValues[key] = returnValue
    }

    func setError<Request: NetworkRequest>(_ error: Error, for requestType: Request.Type) {
        let key = String(describing: Request.self)
        presetErrors[key] = error
    }
}

I guess what I'm looking for is a way to at least erase the generic type to Any, and a bonus would be a way to say "if the Request is of Type X with ResponseType.Y, then the stored return value is ResponseType.Y". Is there any way to do this using a stencil for Sourcery?

Cross-posted on Stack Overflow as well. Mainly out of curiosity how alive SO still is: https://stackoverflow.com/questions/79248429/sourcery-stencil-for-generic-function-using-associatedtype

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant