Swift Codable protocol with recursive enums

2019-03-21 16:52发布

问题:

Let's say that I have a model like the following, which allows me to build a tree of Foo objects.

struct Foo {

    var kind : Kind

    enum Kind {
        case node([Foo])
        case leaf
    }
}

How can I make this Codable, specifically for the case node([Foo])?

回答1:

Here's the final struct, based on the answer from @PauloMattos:

Base Foo struct:

struct Foo {

    var name: String
    var kind: Kind

    enum Kind {
        case node([Foo])
        case leaf
    }

    init(name: String, kind: Kind) {
        self.name = name
        self.kind = kind
    }
}

Codable Protocol extension:

extension Foo : Codable {

    enum CodingKeys: String, CodingKey {
        case name
        case nodes
    }

    enum CodableError: Error {
        case decoding(String)
        case encoding(String)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        switch kind {
        case .node(let nodes):
            var array = container.nestedUnkeyedContainer(forKey: .nodes)
            try array.encode(contentsOf: nodes)
            break
        case .leaf:
            break
        }
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // Assumes name exists for all objects
        if let name = try? container.decode(String.self, forKey: .name) {
            self.name = name
            self.kind = .leaf
            if let array = try? container.decode([Foo].self, forKey: .nodes) {
                self.kind = .node(array)
            }
            return
        }
        throw CodableError.decoding("Decoding Error")
    }
}

CustomStringConvertable Protocol extension (to output string from the tree):

extension Foo : CustomStringConvertible {

    var description: String {
        return stringDescription(self)
    }

    private func stringDescription(_ foo: Foo) -> String {
        var string = ""
        switch foo.kind {
        case .leaf:
            return foo.name
        case .node(let nodes):
            string += "\(foo.name): ("
            for i in nodes.indices {
                string += stringDescription(nodes[i])
                // Comma seperate all but the last
                if i < nodes.count - 1 { string += ", " }
            }
            string += ")"
        }
        return string
    }
}

And example testing code:

let a = Foo(name: "A", kind: .leaf)
let b = Foo(name: "B", kind: .leaf)
let c = Foo(name: "C", kind: .leaf)
let d = Foo(name: "D", kind: .node([b, c]))
let root = Foo(name: "ROOT", kind: .node([a, d]))

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(root)
let json = String(data: jsonData, encoding: .utf8)!
print("Foo to JSON:")
print(json)

let decoder = JSONDecoder()
do {
    let foo = try decoder.decode(Foo.self, from: jsonData)
    print("JSON to Foo:")
    print(foo)
} catch {
    print(error)
}

Output:

Foo to JSON:
{
  "name" : "ROOT",
  "nodes" : [
    {
      "name" : "A"
    },
    {
      "name" : "D",
      "nodes" : [
        {
          "name" : "B"
        },
        {
          "name" : "C"
        }
      ]
    }
  ]
}
JSON to Foo:
ROOT: (A, D: (B, C))


回答2:

One possible encoding for the Foo recursive data type could be:

struct Foo: Encodable {
    var name: String // added a per-node payload as well.
    var kind: Kind

    enum Kind {
        case node([Foo])
        case leaf
    }

    enum CodingKeys: String, CodingKey {
        case name
        case nodes
    }

    func encode(to encoder: Encoder) throws {
        var dict = encoder.container(keyedBy: CodingKeys.self)
        try dict.encode(name, forKey: .name)
        switch kind {
        case .node(let nodes):
            var array = dict.nestedUnkeyedContainer(forKey: .nodes)
            try array.encode(contentsOf: nodes)
        case .leaf:
            break // Nothing to encode. 
        }
    }
}

A simple test using the JSON encoder:

let a = Foo(name: "A", kind: .leaf)
let b = Foo(name: "C", kind: .leaf)
let c = Foo(name: "B", kind: .leaf)
let root = Foo(name: "ROOT", kind: .node([a, b, c]))

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(root)
let json = String(data: jsonData, encoding: .utf8)!
print(json)

would then output the following JSON:

{
  "name" : "ROOT",
  "nodes" : [
    {
      "name" : "A"
    },
    {
      "name" : "C"
    },
    {
      "name" : "B"
    }
  ]
}

Conforming to Decodable should follow a similar logic ;)



回答3:

Here is a great post of Decoadable protocol and its usage.

I think at the bottom of the post in the Enum section you can find what you need, but if you don't want to read the article here is the gist which can be helpful.