0

This fails (Non-nominal type 'Any' cannot be extended)

extension Any {
    func literal() -> String {
        if let booleanValue = (self as? Bool) {
            return String(format: (booleanValue ? "true" : "false"))
        }
        else
        if let intValue = (self as? Int) {
            return String(format: "%d", intValue)
        }
        else
        if let floatValue = (self as? Float) {
            return String(format: "%f", floatValue)
        }
        else
        if let doubleValue = (self as? Double) {
            return String(format: "%f", doubleValue)
        }
        else
        {
            return String(format: "<%@>", self)
        }
    }
}

as I would like to use it in a dictionary (self) to xml string factory like

extension Dictionary {
    //  Return an XML string from the dictionary
    func xmlString(withElement element: String, isFirstElement: Bool) -> String {
        var xml = String.init()

        if isFirstElement { xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") }

        xml.append(String(format: "<%@>\n", element))
        for node in self.keys {
            let value = self[node]

            if let array: Array<Any> = (value as? Array<Any>) {
                xml.append(array.xmlString(withElement: node as! String, isFirstElemenet: false))
            }
            else
            if let dict: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>) {
                xml.append(dict.xmlString(withElement: node as! String, isFirstElement: false))
            }
            else
            {
                xml.append(String(format: "<%@>", node as! CVarArg))
                xml.append((value as Any).literal
                xml.append(String(format: "</%@>\n", node as! CVarArg))
            }
        }

        xml.append(String(format: "</%@>\n", element))

        return xml.replacingOccurrences(of: "&", with: "&amp", options: .literal, range: nil)
    }
}

I was trying to reduce the code somehow, as the above snippet is repeated a few times in a prototype I'm building but this is not the way to do it (a working copy with the snippet replicated works but ugly?).

Basically I want to generate a literal for an Any value - previously fetched from a dictionary.

slashlos
  • 913
  • 9
  • 17

2 Answers2

0

It seems like you can't add extensions to Any. You do have some other options though - either make it a function toLiteral(value: Any) -> String, or what is probably a neater solution; use the description: String attribute which is present on all types that conform to CustomStringConvertible, which includes String, Int, Bool, and Float - your code would be simplified down to just xml.append(value.description). You then just have make a simple implementation for any other types that you might get.

Will Richardson
  • 7,780
  • 7
  • 42
  • 56
  • I dismissed description() due to extras but if I get you right, rather than my snippet - which was a distillation but on Any itself, 'll try adding literal() via description() to each type I need. Hopefully others will find this useful. – slashlos Jul 23 '18 at 11:40
0

Ok, finally got this working. First the preliminaries: each of your objects needs to have a dictionary() method to marshal itself. Note: "k.###" are struct static constants - i.e., k.name is "name", etc. I have two objects, a PlayItem and a PlayList:

class PlayItem : NSObject {
    var name : String = k.item
    var link : URL = URL.init(string: "http://")!
    var time : TimeInterval
    var rank : Int
    var rect : NSRect
    var label: Bool
    var hover: Bool
    var alpha: Float
    var trans: Int
    var temp : String {
        get {
            return link.absoluteString
        }
        set (value) {
            link = URL.init(string: value)!
        }
    }

    func dictionary() -> Dictionary<String,Any> {
        var dict = Dictionary<String,Any>()
        dict[k.name] = name
        dict[k.link] = link.absoluteString
        dict[k.time] = time
        dict[k.rank] =  rank
        dict[k.rect] = NSStringFromRect(rect)
        dict[k.label] = label ? 1 : 0
        dict[k.hover] = hover ? 1 : 0
        dict[k.alpha] = alpha
        dict[k.trans] = trans
        return dict
    }
}

class PlayList : NSObject {
    var name : String = k.list
    var list : Array <PlayItem> = Array()

    func dictionary() -> Dictionary<String,Any> {
        var dict = Dictionary<String,Any>()
        var items: [Any] = Array()
        for item in list {
            items.append(item.dictionary())
        }
        dict[k.name] = name
        dict[k.list] = items
        return dict
    }
}

Note any value so marshal has to be those legal types for a dictionary; it helps to have aliases so in the PlayItem a "temp" is the string version for the link url, and its getter/setter would translate.

When needed, like the writeRowsWith drag-n-drop tableview handler, I do this:

    func tableView(_ tableView: NSTableView, writeRowsWith rowIndexes: IndexSet, to pboard: NSPasteboard) -> Bool {

        if tableView == playlistTableView {
            let objects: [PlayList] = playlistArrayController.arrangedObjects as! [PlayList]
            var items: [PlayList] = [PlayList]()
            var promises = [String]()

            for index in rowIndexes {
                let item = objects[index]
                let dict = item.dictionary()
                let promise = dict.xmlString(withElement: item.className, isFirstElement: true)
                promises.append(promise)
                items.append(item)
            }

            let data = NSKeyedArchiver.archivedData(withRootObject: items)
            pboard.setPropertyList(data, forType: PlayList.className())
            pboard.setPropertyList(promises, forType:NSFilesPromisePboardType)
            pboard.writeObjects(promises as [NSPasteboardWriting])
        }
        else
        {
            let objects: [PlayItem] = playitemArrayController.arrangedObjects as! [PlayItem]
            var items: [PlayItem] = [PlayItem]()
            var promises = [String]()

            for index in rowIndexes {
                let item = objects[index]
                let dict = item.dictionary()
                let promise = dict.xmlString(withElement: item.className, isFirstElement: true)
                promises.append(promise)
                items.append(item)
            }

            let data = NSKeyedArchiver.archivedData(withRootObject: items)
            pboard.setPropertyList(data, forType: PlayList.className())
            pboard.setPropertyList(promises, forType:NSFilesPromisePboardType)
            pboard.writeObjects(promises as [NSPasteboardWriting])
        }
        return true
    }

What makes this happen are these xmlString extensions and the toLiteral function - as you cannot extend "Any":

func toLiteral(_ value: Any) -> String {
    if let booleanValue = (value as? Bool) {
        return String(format: (booleanValue ? "1" : "0"))
    }
    else
    if let intValue = (value as? Int) {
        return String(format: "%d", intValue)
    }
    else
    if let floatValue = (value as? Float) {
        return String(format: "%f", floatValue)
    }
    else
    if let doubleValue = (value as? Double) {
        return String(format: "%f", doubleValue)
    }
    else
    if let stringValue = (value as? String) {
        return stringValue
    }
    else
    if let dictValue: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>)
    {
        return dictValue.xmlString(withElement: "Dictionary", isFirstElement: false)
    }
    else
    {
        return ((value as AnyObject).description)
    }
}

extension Array {
    func xmlString(withElement element: String, isFirstElemenet: Bool) -> String {
        var xml = String.init()

        xml.append(String(format: "<%@>\n", element))
        self.forEach { (value) in
            if let array: Array<Any> = (value as? Array<Any>) {
                xml.append(array.xmlString(withElement: "Array", isFirstElemenet: false))
            }
            else
            if let dict: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>) {
                xml.append(dict.xmlString(withElement: "Dictionary", isFirstElement: false))
            }
            else
            {
                xml.append(toLiteral(value))
            }
        }
        xml.append(String(format: "<%@>\n", element))

        return xml
    }
}

extension Dictionary {
    //  Return an XML string from the dictionary
    func xmlString(withElement element: String, isFirstElement: Bool) -> String {
        var xml = String.init()

        if isFirstElement { xml.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") }

        xml.append(String(format: "<%@>\n", element))
        for node in self.keys {
            let value = self[node]

            if let array: Array<Any> = (value as? Array<Any>) {
                xml.append(array.xmlString(withElement: node as! String, isFirstElemenet: false))
            }
            else
            if let dict: Dictionary<AnyHashable,Any> = (value as? Dictionary<AnyHashable,Any>) {
                xml.append(dict.xmlString(withElement: node as! String, isFirstElement: false))
            }
            else
            {
                xml.append(String(format: "<%@>", node as! CVarArg))
                xml.append(toLiteral(value as Any))
                xml.append(String(format: "</%@>\n", node as! CVarArg))
            }
        }

        xml.append(String(format: "</%@>\n", element))

        return xml
    }
    func xmlHTMLString(withElement element: String, isFirstElement: Bool) -> String {
        let xml = self.xmlString(withElement: element, isFirstElement: isFirstElement)

        return xml.replacingOccurrences(of: "&", with: "&amp", options: .literal, range: nil)
    }
}

This continues another's solution, the toLiteral() suggestion above, in hopes it helps others.

Enjoy.

slashlos
  • 913
  • 9
  • 17