blackjacx
blackjacx

Reputation: 10510

How to simplify almost equal enum extensions in Swift

I have extensions for about 20 enums which look like this:

extension CurrencyValue : JSONDecodable {
    static func create(rawValue: String) -> CurrencyValue {
        if let value = CurrencyValue(rawValue: rawValue) {
            return value
        }
        return .unknown
    }

    static func decode(j: JSONValue) -> CurrencyValue? {
        return CurrencyValue.create <^> j.value()
    }
}

extension StatusValue : JSONDecodable {
    static func create(rawValue: String) -> StatusValue {
        if let value = StatusValue(rawValue: rawValue) {
            return value
        }
        return .unknown
    }

    static func decode(j: JSONValue) -> StatusValue? {
        return StatusValue.create <^> j.value()
    }
}

They are almost the same except the enum type name and I have 20 of them - thats obviously very stupid. Does anybody have an idea how to reduce them to one, maybe by using generics? I have no idea at the moment.

UPDATE

The enums are as simple as this:

enum CurrencyValue : String {
    case EUR = "EUR"
    case unknown = "unknown"
}

enum StatusValue : String {
    case ok = "ok"
    case pending = "pending"
    case error = "error"
    case unknown = "unknown"
}

And lets assume the following:

  1. Every ENUM has the .unknown case
  2. I need to exchange the enum type in the extension with something generic.

There must be some trick to not implement the same extension multiple times and just alter the type.

UPDATE

As it s stated by Gregory Higley below I use the JSON lib Argo. You can read about the operators there.

Upvotes: 6

Views: 2547

Answers (3)

rokob
rokob

Reputation: 605

For anyone else coming along, the most up to date answer is to follow: https://github.com/thoughtbot/Argo/blob/td-decode-enums/Documentation/Decode-Enums.md

Essentially, for the enums

enum CurrencyValue : String {
    case EUR = "EUR"
    case unknown = "unknown"
}

enum StatusValue : String {
    case ok = "ok"
    case pending = "pending"
    case error = "error"
    case unknown = "unknown"
}

You only need to do

extension CurrencyValue: Decodable {}
extension StatusValue: Decodable {}

Also as it seems to have been pointed out repeatedly, if you just get rid of the .Unknown field and instead use optionals you can use the builtin support for enums as a RawRepresentable type.

See also: https://github.com/thoughtbot/Argo/blob/master/Argo/Extensions/RawRepresentable.swift for the implementation that makes this possible.

Upvotes: 1

Gregory Higley
Gregory Higley

Reputation: 16598

The essence of your question is that you want to avoid writing boilerplate code for all of these enumerations when implementing Argo's JSONDecodable. It looks like you've also added a create method, which is not part of the type signature of JSONDecodable:

public protocol JSONDecodable {
  typealias DecodedType = Self
  class func decode(JSONValue) -> DecodedType?
}

Unfortunately this cannot be done. Swift protocols are not mixins. With the exception of operators, they cannot contain any code. (I really hope this gets "fixed" in a future update of Swift. Overridable default implementations for protocols would be amazing.)

You can of course simplify your implementation in a couple of ways:

  1. As Tony DiPasquale suggested, get rid of .unknown and use optionals. (Also, you should have called this .Unknown. The convention for Swift enumeration values is to start them with a capital letter. Proof? Look at every enumeration Apple has done. I can't find a single example where they start with a lower case letter.)
  2. By using optionals, your create is now just a functional alias for init? and can be implemented very simply.
  3. As Tony suggested, create a global generic function to handle decode. What he did not suggest, though he may have assumed it was implied, was to use this to implement JSONDecodable.decode.
  4. As a meta-suggestion, use Xcode's Code Snippets functionality to create a snippet to do this. Should be very quick.

At the asker's request, here's a quick implementation from a playground. I've never used Argo. In fact, I'd never heard of it until I saw this question. I answered this question simply by applying what I know about Swift to an examination of Argo's source and reasoning it out. This code is copied directly from a playground. It does not use Argo, but it uses a reasonable facsimile of the relevant parts. Ultimately, this question is not about Argo. It is about Swift's type system, and everything in the code below validly answers the question and proves that it is workable:

enum JSONValue {
    case JSONString(String)
}

protocol JSONDecodable {
    typealias DecodedType = Self
    class func decode(JSONValue) -> DecodedType?
}

protocol RawStringInitializable {
    init?(rawValue: String)
}

enum StatusValue: String, RawStringInitializable, JSONDecodable {
    case Ok = "ok"
    case Pending = "pending"
    case Error = "error"

    static func decode(j: JSONValue) -> StatusValue? {
        return decodeJSON(j)
    }
}

func decodeJSON<E: RawStringInitializable>(j: JSONValue) -> E? {
    // You can replace this with some fancy Argo operators,
    // but the effect is the same.
    switch j {
    case .JSONString(let string): return E(rawValue: string)
    default: return nil
    }
}

let j = JSONValue.JSONString("ok")
let statusValue = StatusValue.decode(j)

This is not pseudocode. It's copied directly from a working Xcode playground.

If you create the protocol RawStringInitializable and have all your enumerations implement it, you will be golden. Since your enumerations all have associated String raw values, they implicitly implement this interface anyway. You just have to make the declaration. The decodeJSON global function uses this protocol to treat all of your enumerations polymorphically.

Upvotes: 3

Tony DiPasquale
Tony DiPasquale

Reputation: 136

If you can provide an example of an enum as well it might be easier to see what could be improved.

Just looking at this I would say you might want to consider removing .unknown from all the enums and just have the optional type represent an .unknown value with .None. If you did this you could write:

extension StatusValue: JSONDecodable {
    static func decode(j: JSONValue) -> StatusValue? {
        return j.value() >>- { StatusValue(rawValue: $0) }
    }
}

No need for all the create functions now. That will cut down on a lot of the duplication.

Edit:

You could possibly use Generics. If you create a protocol DecodableStringEnum like so:

protocol DecodableStringEnum {
  init?(rawValue: String)
}

Then make all your enums conform to it. You don't have to write any more code because that init comes with raw value enums.

enum CreatureType: String, DecodableStringEnum {
  case Fish = "fish"
  case Cat = "cat"
}

Now write a global function to handle all those cases:

func decodeStringEnum<A: DecodableStringEnum>(key: String, j: JSONValue) -> A? {
  return j[key]?.value() >>- { A(rawValue: $0) }
}

Finally, in Argo you can have your creature decode function look like this:

static func decode(j: JSONValue) -> Creature? {
  return Creature.create
    <^> j <| "name"
    <*> decodeStringEnum("type", j)
    <*> j <| "description"
}

Upvotes: 1

Related Questions