Home > Mobile >  Is it possible for a Swift type to be inferred by "pulling out" a Type value from a generi
Is it possible for a Swift type to be inferred by "pulling out" a Type value from a generi

Time:01-31

Introduction

(Apologies if the title is confusing, but I explain the question better here!)

I'm building a networking library that can perform JSON decoding on its responses.

Host apps adopting this library will create enums conforming to NetLibRoute. All that currently does is enforce the presence of asURL:

public protocol NetLibRoute {
    var asURL: URL { get throws }
}

In a host app, I have a routing system that enforces API structure at the compiler-level (via enums and associated values) for each endpoint, like this:

enum Routes: NetLibRoute {
    case people(Int?)
    // Other routes go here, e.g.:
    // case user(Int)
    // case search(query: String, limit: Int?)
    
    var asURL: URL {
        let host = "https://swapi.dev/"
        
        let urlString: String
        
        switch self {
        case let .people(personID):
            if let personID {
                urlString = host   "api/people/\(personID)"
            } else {
                urlString = host   "api/people/"
            }
        // Build other URLs from associated values
        }
        
        return URL(string: urlString)!
    }
}

I also want each enum to be associated with a certain Codable type. I can do that, of course, by modifying the Route protocol declaration to also require a type conforming to Decodable:

protocol NetLibRoute {
    var asURL: URL { get throws }
    var decodedType: Decodable.Type { get } // This
}

And a matching computed property in my Routes enum:

var decodedType: Decodable.Type {
    switch self {
    case .people(_):
        return Person.self
    // And so on
    }
}

The Problem

Currently, my networking code has a declaration something like this:

public static func get<T>(route: Route,
                          type: T.Type) async throws -> T where T: Decodable {
    // performing request on route.asURL
    // decoding from JSON as T or throwing error
    // returning decoded T
}

Which lets me call it like this:

let person = try await NetLib.get(route: Routes.people(1), type: Person.self)

However, this redundancy (and potential human error from mismatching route and type) really irks me. I really want to be able to only pass in a route, and have the resulting type be inferred from there.

Is there some way to get the compiler to somehow check the NetLibRoute enum and check its decodedType property, in order to know what type to use?

Ultimately, I want this networking function to take one parameter (a route) and infer the return type of that route (at compile-time, not with fragile runtime hacks or !s), and return an instance of the type.

Is this possible?


Potential Alternatives?

I'm also open to alternative solutions that may involve moving where the get function is called from.

For example, calling this get function on a route itself to return the type:

let person = try await Routes.people(1).get(type: Person.self) // Works, but not optimal
let person = try await Routes.people(1).get() // What I want

Or even on the type itself, by creating a new protocol in the library, and then extending Decodable to conform to it:

public protocol NetLibFetchable {
    static var route: NetLibRoute { get }
}

extension Decodable where Self: NetLibFetchable {
    public static func get<T>() async throws -> T where Self == T, T: Decodable {
    // Call normal get function using inferred properties
    return try await NetLib.get(route: route,
                                type: T.self)
}

Which indeed lets me call like this:

let person = try await Person.get() // I can't figure out a clean way to pass in properties that the API may want, at least not without once again passing in Routes.people(1), defeating the goal of having Person and Routes.people inherently linked.

While this eliminates the issue of type inference, the route can no longer be customized at call-time, and instead is stuck like this:

extension Person: XMHTTP.Fetchable {
    public static var route: XMHTTP.Route {
        Routes.people(1) // Can't customize to different ID Ints anymore!
    }
}

Which makes this particular example a no-go, and leaves me at a loss.


Appreciation

Anyway, thank you so much for reading, for your time, and for your help.

I really want this library to be as clean as possible for host apps interacting with it, and your help will make that possible.

CodePudding user response:

I think the problem lays in this function.

public static func get<T>(route: Route,
                      type: T.Type) async throws -> T where T: Decodable {
    // performing request on route.asURL
    // decoding from JSON as T or throwing error
    // returning decoded T
}

On the first hand, it uses concretions instead of abstractions. You shouldn't pass a Route here, it should use your protocol NetLibRoute instead.

On the other hand, I think that the type param is not needed. Afaik you can get the Type to Decode with the var:

NetLibRoute.decodedType

Am I missing something on this matter?


Apart from that, I'd rather go with struct instead of enum when trying to implement the Routes (concretions). Enums cannot be extended. So you won't be allowing the creation of new requests in client side, only in the library.

I hope I've helped.

PS: Some time ago I made this repo. Maybe that could help you (specially this class). I used Combine instead of async/await, but it's not relevant to what you need.

CodePudding user response:

Are you wedded to the idea of using an enum? If not, you can do pretty much what you want by giving each enum value its own type and using an associated type to do what you want.

public protocol NetLibRoute
{
    var asURL: URL { get throws }

    associatedtype Decoded: Decodable
}

struct Person: Decodable
{
    var name: String
}

struct Login: Decodable
{
    var id: String
}

struct People: NetLibRoute
{
    typealias Decoded = Person

    var id: Int

    var asURL: URL { return URL(filePath: "/") }

}

struct User: NetLibRoute
{
    typealias Decoded = Login

    var id: String

    var asURL: URL { return URL(filePath: "/") }
}

func get<N: NetLibRoute>(item: N) throws -> N.Decoded
{
    let data = try Data(contentsOf: item.asURL)
    return try JSONDecoder().decode(N.Decoded.self, from: data)
}

let thing1 = try get(item: People(id: 1))
let thing2 = try get(item: User(id: "foo"))

Where you might have had a switch before to do different things with different Routes you would now use a function with overloaded arguments.

func doSomething(thing: Person)
{
    // do something for a Person
}

func doSomething(thing: Login)
{
    // do something else for a Login
}

doSomething(thing: thing1)
doSomething(thing: thing2)
  • Related