Reputation: 10510
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:
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
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
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:
.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.)create
is now just a functional alias for init?
and can be implemented very simply.decode
. What he did not suggest, though he may have assumed it was implied, was to use this to implement JSONDecodable.decode
. 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
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