2
\$\begingroup\$

I'm currently building a small web stack in swift for use in my iOS apps. It consists of multiple components and I want to make it resource specific; To that end, I have a WebService class composed of two parts; a part that handles the request logic, and a part that handles the parsing.

The catch here is that the parsing is defined by a ParserType protocol with an associated type ModelType, which needs to be specified by the adopter. It parses Any? to that ModelType:

public protocol ParserType {

    associatedtype ModelType

    func parse(_ data: Any?) throws -> ModelType
}

This is no problem on its own; a concrete parser could look like this (just forgive me the force casting here):

public class SimpleJSONParser<T: Codable>: ParserType {

    public typealias ModelType = T
    private let jsonDecoder: JSONDecoder

    public init(jsonDecoder: JSONDecoder = JSONDecoder()) {
        self.jsonDecoder = jsonDecoder
    }


    open func parse(_ data: Any?) throws -> T {
        let response = try jsonDecoder.decode(T.self, from: data as! Data)
        return response
    }
}

Now we have a concrete JSON parser that tries to parse Any? to T. That's cool, so we want to inject it into WebService, so it can be used. It looks like this:

public class WebService<ModelType, Parser: ParserType> {

    public typealias ModelType = Parser.ModelType
    public typealias WebServiceCompletion = (Result<ModelType>) -> Void

    private let endpoint: String
    private let parser: Parser
    private let transport: ServiceTransport

    public init(transport: ServiceTransport, endpoint: String, parser: Parser) {
        self.transport = transport
        self.endpoint = endpoint
        self.parser = parser
    }

    @discardableResult
    open func fetch(path: String? = nil, headers: [String: String]? = nil, parameters: [String: String]? = nil, autoResumeTask: Bool = true, completion: @escaping WebServiceCompletion) -> ServiceTransportTask {

        let endpointToUse = endpoint + (path ?? "")

        let task = transport.perform(action: .get, path: endpointToUse, parameters: parameters, headers: headers, body: nil)
                .completion { response in
                    switch response {
                    case .failure(let error):
                        completion(.failure(error))
                    case .success(let data):
                        do {
                            let result: ModelType = try self.parser.parse(data)
                            completion(.success(result))
                        } catch let error {
                            completion(.failure(error))
                        }
                    }
                }

        if autoResumeTask {
            task.resume()
        }

        return task
    }
}

The issue I have is that when I want to use this, I have to specify to the WebService that I want to use it for a type CategoryArray for example, with a parser that parses JSON to CategoryArray, i.e. JSONParser<CategoryArray>, and then also inject such a parser into the WebService:

let transport = HTTPTransport(baseURL: URL(string: baseURLString)!)
let parser = JSONParser<CategoryArray>()
let webService: WebService<CategoryArray, JSONParser<CategoryArray>> webService = WebService(transport: transport, endpoint: endPoint, parser: parser)

It feels a bit weird to me in the sense that I have to define the model type to use in both WebService and Parser, and also define the parser type.

Is my approach too generic, or can I factor out something here?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Turns out I can just use the generic ModelType defined in the ParserType, and I don't need the same generic parameter in WebService;

public class WebService<Parser: ParserType> {
...

The typealias is just for convenience, to not type ParserType.ModelType everywhere but just ModelType.

Results in this call site:

let transport = HTTPTransport(baseURL: URL(string: baseURLString)!)
let parser = JSONParser<CategoryArray>()
let webService: WebService<JSONParser<CategoryArray>> webService = WebService(transport: transport, endpoint: endPoint, parser: parser)

(And because of type inference, the last line can be reduced to let webService = WebService(transport: transport, endpoint: endPoint, parser: parser) which is neat)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.