Extensible enums
See original GitHub issueFor interoperation with poorly specced or flexible downstream APIs, clients should be able to represent a branch of an enumeration that isn’t known. This permits accepting unknown branches and doing work with them without rejecting the response from client calls (currently, enumeration lookup failures result in a decoder failure for clients).
Currently, generated enumerations look like this:
sealed abstract class Foo(val value: String) { override def toString: String = value.toString }
object Foo {
object members {
case object Bar extends Foo("Bar")
}
val Bar: Foo = members.Bar
val values = Vector(Bar)
def parse(value: String): Option[Foo] = values.find(_.value == value)
implicit val encodeFoo: Encoder[Foo] = Encoder[String].contramap(_.value)
implicit val decodeFoo: Decoder[Foo] = Decoder[String].emap(value => parse(value).toRight(s"$value not a member of Foo"))
implicit val addPathFoo: AddPath[Foo] = AddPath.build(_.value)
implicit val showFoo: Show[Foo] = Show.build(_.value)
}
A possible alteration to accept unknown members (currently unsure if this should be opt-in or opt-out, definitely needs more thought):
@@ -2,10 +2,12 @@
object Foo {
object members {
case object Bar extends Foo("Bar")
+ class Unknown private[Foo] (value: String) extends Foo(value)
}
val Bar: Foo = members.Bar
+ def Unknown(value: String): Foo = new members.Unknown(value)
val values = Vector(Bar)
- def parse(value: String): Option[Foo] = values.find(_.value == value)
+ def parse(value: String): Option[Foo] = values.find(_.value == value).orElse(new members.Unknown(value))
implicit val encodeFoo: Encoder[Foo] = Encoder[String].contramap(_.value)
implicit val decodeFoo: Decoder[Foo] = Decoder[String].emap(value => parse(value).toRight(s"$value not a member of Foo"))
implicit val addPathFoo: AddPath[Foo] = AddPath.build(_.value)
Initially I thought the Unknown
branch should not expose a constructor, as it would prevent invalid data from being supplied by the clients into downstream services. This has the unfortunate side-effect of not allowing services to round-trip to persistence layers; consider receiving values of an array via client API call, writing those values into a database, then attempting to use those values to make future API calls.
Issue Analytics
- State:
- Created 5 years ago
- Comments:12 (12 by maintainers)
Top GitHub Comments
fyi, I am aware of prior art in two open source projects:
ApiBuilder uses UNDEFINED as the marker for unknown enumeration values:
https://github.com/apicollective/apibuilder-generator/blob/main/scala-generator/src/main/scala/models/generator/ScalaEnums.scala
Here is example output from the ApiBuilder Scala generator:
https://app.apibuilder.io/flow/delta/0.8.36/scala_models/FlowDeltaV0Models.scala
AWS SDK for Java (version 2) defines a special Java enum value called UNKNOWN_TO_SDK_VERSION
https://github.com/aws/aws-sdk-java-v2/blob/master/codegen/src/main/java/software/amazon/awssdk/codegen/poet/common/AbstractEnumClass.java#L56
Right, but that’s opt-in behavior