MongoTheGeek
MongoTheGeek

Reputation: 294

Swift Failable initializer from XML

Let me start by saying what I have works...

    let a:String
    let b:Int
    let c:Double

    init?(doc:XMLDocument){
        guard let tempString = (try? doc.objects(forXQuery: "path/path/A").first as? XMLNode)?.stringValue else {return nil}
        self.a = tempString
        guard let tempString2 = (try? doc.objects(forXQuery: "path/B").first as? XMLNode)?.stringValue else {return nil}
        guard let tempInt = Int(tempString2) else {return nil}
        self.b = tempInt
        guard let tempString3 = (try? doc.objects(forXQuery: "path/path/C").first as? XMLNode)?.stringValue else {return nil}
        guard let tempDouble = Double(tempString3) else {return nil}
        self.c = tempDouble
    }
}

I am not happy with it. I can simplify by doing null check and reusing a temporary variable

    init?(doc:XMLDocument){
        var tempString:String?  = (try? doc.objects(forXQuery: "path/path/A").first as? XMLNode)?.stringValue
        if tempString != nil {self.a = tempString!} else {return nil}
        tempString = (try? doc.objects(forXQuery: "path/B").first as? XMLNode)?.stringValue
        if tempString != nil && Int(tempString!) != nil {self.b = Int(tempString!)!} else {return nil}
        tempString = (try? doc.objects(forXQuery: "path/path/C").first as? XMLNode)?.stringValue
        if tempString != nil && Double(tempString!) != nil {self.c = Double(tempString!)!} else {return nil}
    }

Ideally I want to be able to assign the properties as I check them without going through some sort of temporary variable.

something like:

struct Foo {
    let a:String
    let b:Int
    let c:Double

    init?(doc:XMLDocument){
        do {
            if case a = try (doc.objects(forXQuery: "path/path/A").first as? XMLNode)?.stringValue { } else {return nil}
            if case b = Int(try (doc.objects(forXQuery: "path/path/B").first as? XMLNode)?.stringValue ?? "ZZZ") { } else {return nil}
            if case c = Double(try (doc.objects(forXQuery: "path/path/C").first as? XMLNode)?.stringValue ?? "ZZZ") { } else {return nil}
        } catch {
            return nil
        }
     }
}

This errors with use of self without initializing all stored properties.

Is there a better way to do it?

Upvotes: 0

Views: 48

Answers (2)

MongoTheGeek
MongoTheGeek

Reputation: 294

building off of Kamran's answer I got this.


extension XMLDocument {
    func firstNode(forXQuery: String) throws -> XMLNode{
        do {
            guard let temp = try self.objects(forXQuery: forXQuery).first as? XMLNode else {throw InitError() }
            return temp
        } catch {
            throw error
        }
    }
}

extension XMLNode {
    func intValue() throws -> Int {
        guard let tempString = self.stringValue else {throw InitError()}
        guard let tempInt = Int(tempString) else {throw InitError()}
        return tempInt
    }
}
extension XMLNode {
    func stringValue() throws -> String {
        guard let tempString = self.stringValue else {throw InitError()}
        return tempString
    }
}
extension XMLNode {
    func doubleValue() throws -> Double {
        guard let tempString = self.stringValue else {throw InitError()}
        guard let tempDouble = Double(tempString) else {throw InitError()}
        return tempDouble
    }
}
extension XMLNode {
    func floatValue() throws -> Float {
        guard let tempString = self.stringValue else {throw InitError()}
        guard let tempFloat = Float(tempString) else {throw InitError()}
        return tempFloat
    }
}

which pretties my initializer to

struct Foo {
    let a:String
    let b:Int
    let c:Double

    init?(doc:XMLDocument){
        do {
            a = try doc.firstNode(forXQuery: "path/path/a").stringValue()
            b = try doc.firstNode(forXQuery: "path/path/b").intValue()
            c = try doc.firstNode(forXQuery: "path/path/c").doubleValue()
        } catch {
            return nil
        }
     }
}

Much better, but it still feels loose.

Upvotes: 0

Kamran
Kamran

Reputation: 15248

I would suggest adding some helpful extensions to XMLDocument and XMLNode as below,

extension XMLDocument {

    public func xmlNode(forXQuery query: String) throws -> XMLNode? {
        return try self.objects(forXQuery: query).first as? XMLNode
    }
}

extension XMLNode {

    public var intValue: Int? {
        if let value = self.stringValue, let intValue = Int(value) {
            return intValue
        }
        return nil
    }

    public var doubleValue: Double? {
        if let value = self.stringValue, let doubleValue = Double(value) {
            return doubleValue
        }
        return nil
    }
}

So, now your struct will look like this,

struct Foo {
    let a: String
    let b: Int
    let c: Double

    init?(doc: XMLDocument) {
        guard let a = try? doc.xmlNode(forXQuery: "path/path/A")?.stringValue,
            let b = try? doc.xmlNode(forXQuery: "path/B")?.intValue,
            let c = try? doc.xmlNode(forXQuery: "path/path/C")?.doubleValue else { return nil }
        self.a = a
        self.b = b
        self.c = c
    }
}

Upvotes: 1

Related Questions