CreateAPI: `enum` unknown values

Forced unknown

Since we are creating API interfaces, new cases may be created from the server that aren’t available on a current version of a developer’s SDK or even their available schema. Only Apple can create nonfrozen enums by my understanding so developers cannot use @unknown default. So, if an app with an out-of-date API schema attempts to decode an unknown case for a property or return type that cannot be optional, it will crash.

To fix this issue, create a new config option enumForceUnknown: true that adds a new case to every enum which represents an unknown case. We would decode to this unknown case if there is no properly expected case currently defined. The name is open for improvement, but I think that it should stand out from typical Swift formatting in a way that ensures it will be different from any case in the schema and tells the developer that this is strongly unknown.

enum MyTypes {
    case foo
    case bar
    case forced_unknown // added unknown case
}

Organization

Smaller pitch for an organization option to put enums in a separate folder: Enums. Just as an organizational measure this can help a lot to separate the struct/class entities from other Swift types to the developer. This would only apply to the split generation and remain as an option. Or, could also apply to merged generation with a separate Enums.swift file.

MyAPI
  - Entities/
  - Paths/
  - Enums/
  AnyJson.swift

Potentially split generation might have more customization options, so we create a new section in the config for each generation type. This would help with https://github.com/CreateAPI/CreateAPI/issues/47.

split:
  - enumFolder: true
  // other options

There would also be a separate section for merged with whatever options it would potentially have. The existence of these two sections together would throw an incorrect config error. This was just a loose idea, needs much improvement.

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 1
  • Comments: 16 (13 by maintainers)

Most upvoted comments

Sensible to me

It feels like it would make sense to incorporate enums under generate regardless to if we bring in more options or not actually. I’ll probably go ahead and do that in my change anyway unless there is a good reason not to?

I see value now in creating the extensions from a RawRepresentable. That sounds good 👍

Only Apple can create nonfrozen enums by my understanding so developers cannot use @unknown default.

Any vendor can compile their library using Library evolution using BUILD_LIBRARY_FOR_DISTRIBUTION (although I’m not sure how to do this with swift packages).

Using such library would mean that by default any enum within that module will be treated as unfrozen which would require the @unknown default statement in switch cases unless the enum was annotated with @frozen.

But then again, it’s probably unlikely that anybody is ever going to use library evolution.

So, if an app with an out-of-date API schema attempts to decode an unknown case for a property or return type that cannot be optional, it will crash.

This to be honest sounds like a problem outside of CreateAPI because the API being consumed isn’t being versioned properly. The JSON Schema/OpenAPI specification does not support any differentiation between frozen or unfrozen enums and technically when you are using a different schema, it should be versioned differently.

We have this discussion a lot in my company and make it clear to api engineers that enums should be considered frozen for the current API version so adding cases is considered a breaking change.


That being said, if we really did want to support flexibility then instead I recommend generating RawRepresentable structs instead of enums. For example:

enum MyTypes: String {
    case foo
    case bar
}

// usage

switch value {
case .foo:
    print("Do foo")
case .bar:
    print("Do bar")
}

would become this:

struct MyTypes: RawRepresentable {
    let rawValue: String
    
    init(rawValue: String) {
        self.rawValue = rawValue
    }

    static let foo = Self(rawValue: "foo")
    static let bar = Self(rawValue: "bar")
}

// usage

switch value {
case .foo:
    print("Do foo")
case .bar:
    print("Do bar")
case MyType(rawValue: "baz"):
    print("Do undocumented baz")
default:
    print("Do something else")
}

The generateEnums option could instead become something like enumDecoration with accepted values of none, enum and rawRepresentable. Maybe we could come up with something better, but something along those lines.