-3

Say you have a simple

struct Stuff  // :Codable ???
 {
 a: String
 b: String
 c: String
}
var stuff: Stuff

You have messages that look like this

msg = "b Peach"

in the example stuff.b should be set to "Peach".

(That sentence is explicit. So, stuff.a and stuff.c would not be changed.)

Naive code would have a long switch statement ...

func processMessage
   vv = msg.split
   switch vv[0] {
     case "a": stuff.a = vv[1]
     case "b": stuff.b = vv[1]
     case "c": stuff.c = vv[1]
     default: print incorrect key!
   }

Note that that code would have stuff.b set to "Peach", stuff.a and stuff.c would not be changed.

Of course, this is very clumsy.

Surely there's a better way to do this in Swift - perhaps with Decodable ?

--

PS Obviously you can just use a dictionary. Not the point of the questions, thanks.

Fattie
  • 27,874
  • 70
  • 431
  • 719
  • 1
    I think it would be easier to help if your code was written out completely and/or provided a partial attempted solution. Right now the pseudocode is a bit unclear and hard to understand. – Michael Fourre Sep 11 '19 at 18:55
  • In your example, what should `a` and `b` be set to? Can only one of these three values be set? Or is this something that mutates an existing struct? Saying "Codable" sounds like it should encode and decode an entire type, but your example doesn't seem to do that. – Rob Napier Sep 11 '19 at 19:41
  • This might help. https://stackoverflow.com/questions/46039049/advantage-of-key-value-coding-in-swift-4 I personally think Joakim is on the right track, but it looks like you're after something else. Someone (you?) downvoted him. – joehinkle11 Sep 11 '19 at 20:16
  • @RobNapier, right, in the example "b Peach" would only decode (if you will) .b and not touch the other two, as it says in the question. (And as the example Naive Code would do.) Of course you're right that `JSONDecoder` would do "all three" (settings the others to nil), but, perhaps in a custom Decoder it would "know" to not change items that are not present. (I, uh, do not have a clue how to make a custom Decoder, so, IDK if that's viable.) – Fattie Sep 11 '19 at 20:39
  • 1
    How about using an `enum` instead? – Mojtaba Hosseini Sep 11 '19 at 20:41
  • that sounds like a fantastic idea @MojtabaHosseini but I dunno how to get from a String to an enum case. – Fattie Sep 11 '19 at 20:43
  • If you want an enum, this question is very different than what you've written here. Your "naive code" doesn't match what you're describing. Do you want to create a new value based on a string encoding (i.e. "decode it"), or do you want to update some existing value based on a string command? Your "processMessage" function is the latter, but your description seems to be the former. – Rob Napier Sep 11 '19 at 20:48
  • Maybe the confusion is that in Swift, "decode" means "create a brand new value from some serialization." What you seem to be asking for is a command language that updates an existing value (which is what I describe in my answer). – Rob Napier Sep 11 '19 at 20:50

3 Answers3

2

Given your sketch, it doesn't seem like you actually want to decode anything. It looks like you want to update something that already exists, so Codable isn't the right model. What you likely want are keyPaths. The most straight-forward (and flexible) way to do that would be to just name them explicitly:

static var keyMap: [String : WritableKeyPath<Stuff, String>] {
    [ "a": \.a,
      "b": \.b,
      "c": \.c,
    ]
}

It may be possible to create this automatically through Mirror, but I'm not aware of a good way to do that. With that, the update is fairly straight-forward:

  • Split up the message
  • Choose the right keyPath
  • Use the keyPath subscript to assign the new value

Here's the code.

extension Stuff {

    static var keyMap: [String : WritableKeyPath<Stuff, String>] { ... }

    struct BadMessage: Error  {}

    mutating func update(with message: String) throws {
        let comp = message.split(separator: " ")

        guard comp.count == 2, let key = Self.keyMap[String(comp[0])] else {
            throw BadMessage()
        }

        let value = String(comp[1])

        self[keyPath: key] = value
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Rob, I really appreciate it can be done with key paths, but, I guess unfortunately one is still stuck typing out / checking each change in the struct during development – Fattie Sep 11 '19 at 20:59
1

One approach could be to convert your string to json, like this

let msg = "b Peach"
let arr = msg.split(separator: " ")

let out = "{\"\(arr[0])\": \"\(arr[1])\"}"

out is now {"b": "Peach"}

Then you can handle it like a normal json decoding

struct Stuff: Decodable {
    let a: String?
    let b: String?
    let c: String?
}

let data = out.data(using: .utf8)!

do {
    let result = try JSONDecoder().decode(Stuff.self, from: data)
} catch {
    error
}
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • btw if you do this, it will erase the others (right?) so you have to bounce it to another Stuff. (as one annoyingly has to do with arriving json in some cases :/ ) – Fattie Sep 11 '19 at 20:46
  • This won't erase anything. It decodes a brand new `Stuff` struct. What do you mean by "bounce?" – Rob Napier Sep 11 '19 at 20:49
  • to achieve what it says in `func processMessage` and what is described in the question, you'd have to take the brand new `Stuff` and merge it in to your "current" one. (as I say, exactly as one annoyingly has to do with arriving json, in some cases :/ ) – Fattie Sep 11 '19 at 20:56
1

I meant Something like this:

struct Stuff {
    let kind: Kind
    let value: String

    init(from text: String) throws {
        let components = text.components(separatedBy: " ")
        guard let rawKind = components.first else { throw Error.noKind }
        guard let kind = Kind(rawValue: rawKind) else { throw Error.unknownKind(rawKind) }
        self.kind = kind
        value = components.dropFirst().joined(separator: " ")
    }

    enum Kind: String { case a, b, c }
    enum Error: Swift.Error {
        case noKind
        case unknownKind(String)
    }
}

Also the initializer easily could be replace by a decoder initializer if needed like this:

init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    let text = try container.decode(String.self)
    try self.init(from: text)
}

But note that you CAN'T directly decode Stuff.self from a String, because it's not a json. but YOU CAN use it inside a wrapper json.

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278