Let's start with the problem I'm trying to solve. I'm parsing an XML document into a hierarchy of model objects. All of the model objects have a common base class with a set of common properties. Then each specific model class has a few additional properties.
Here's a simplified example of a few model classes:
class Base {
var id: String?
var name: String?
var children = [Base]()
}
class General: Base {
var thing: String?
}
class Specific: General {
var boring: String?
}
class Other: Base {
var something: String?
var another: String?
}
The part I'm having issue with is implementing a clean way to write the XML parser classes to deal with this model hierarchy. I'm attempting to write a parser hierarchy that matches the model hierarchy. Here's my attempt:
protocol ObjectParser {
associatedtype ObjectType
func createObject() -> ObjectType
func parseAttributes(element: XMLElement, object: ObjectType)
func parseElement(_ element: XMLElement) -> ObjectType
}
class BaseParser: ObjectParser {
typealias ObjectType = Base
var shouldParseChildren: Bool {
return true
}
func createObject() -> Base {
return Base()
}
func parseAttributes(element: XMLElement, object: Base) {
object.id = element.attribute(forName: "id")?.stringValue
object.name = element.attribute(forName: "name")?.stringValue
}
func parseChildren(_ element: XMLElement, parent: Base) {
if let children = element.children {
for child in children {
if let elem = child as? XMLElement, let name = elem.name {
var parser: BaseParser? = nil
switch name {
case "general":
parser = GeneralParser()
case "specific":
parser = SpecificParser()
case "other":
parser = OtherParser()
default:
break
}
if let parser = parser {
let res = parser.parseElement(elem)
parent.children.append(res)
}
}
}
}
}
func parseElement(_ element: XMLElement) -> Base {
let res = createObject()
parseAttributes(element: element, object: res)
if shouldParseChildren {
parseChildren(element, parent: res)
}
return res
}
}
class GeneralParser: BaseParser {
typealias ObjectType = General
override func createObject() -> General {
return General()
}
func parseAttributes(element: XMLElement, object: General) {
super.parseAttributes(element: element, object: object)
object.thing = element.attribute(forName: "thing")?.stringValue
}
}
class SpecificParser: GeneralParser {
typealias ObjectType = Specific
override func createObject() -> Specific {
return Specific()
}
func parseAttributes(element: XMLElement, object: Specific) {
super.parseAttributes(element: element, object: object)
object.boring = element.attribute(forName: "boring")?.stringValue
}
}
And there is OtherParser
which is the same as GeneralParser
except replace General
with Other
. Of course there are many more model objects and associated parsers in my hierarchy.
This version of the code almost works. You'll notice there is no override
for the parseAttributes
methods in the GeneralParser
and SpecificParser
classes. I think this is due to the different type for the object
argument. The result of this is that the parser specific parseAttributes
methods are not being called from the parseElement
method of BaseParser
. I got around this problem by updating all of the parseAttributes
signatures to:
func parseAttributes(element: XMLElement, object: Base)
Then in the non-Base parsers, I had to use a force-cast (and add override
such as the following in the GeneralParser
:
override func parseAttributes(element: XMLElement, object: Base) {
super.parseAttributes(element: element, object: object)
let general = object as! General
general.thing = element.attribute(forName: "thing")?.stringValue
}
Finally, the question:
How do I eliminate the need for the force-cast in the parseAttributes
method hierarchy and make use of the protocol's associated type? And more general, is this the correct approach to this problem? Is there a more "Swift" way to solve this problem?
Here's some made up XML based on this simplified object model if needed:
<other id="top-level" name="Hi">
<general thing="whatever">
<specific boring="yes"/>
<specific boring="probably"/>
<other id="mid-level">
<specific/>
</other>
</general>
</other>