Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic: support open enums #1244

Merged
merged 5 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
# 0.18.0 (in progress)
# 0.18.1

## Open enum support in Dynamic module

In 0.18.0, support was added for [open enums](https://disneystreaming.github.io/smithy4s/docs/codegen/customisation/open-enums) in smithy4s-generated code. This release extends that support to runtime (dynamic) schemas.

# 0.18.0

## Behavioural changes

Expand Down
3 changes: 3 additions & 0 deletions modules/core/src/smithy4s/schema/EnumValue.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ case class EnumValue[E](
name: String,
hints: Hints
) {
def map[A](f: E => A): EnumValue[A] =
copy(value = f(value))

def transformHints(f: Hints => Hints): EnumValue[E] =
copy(hints = f(hints))
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,11 @@ private[dynamic] object Compiler {
shapeId: ShapeId,
traits: Map[IdRef, Document],
lSchema: Eval[Schema[A]]
): Unit =
updateWithHints(shapeId, allHints(traits), lSchema)

private def updateWithHints[A](
shapeId: ShapeId,
hints: Hints,
lSchema: Eval[Schema[A]]
): Unit = {
schemaMap += (shapeId -> lSchema.map { sch =>
sch
.withId(shapeId)
.addHints(hints)
.addHints(allHints(traits))
.asInstanceOf[Schema[DynData]]
})
}
Expand All @@ -190,12 +183,6 @@ private[dynamic] object Compiler {
schema: Schema[A]
): Unit = update(shapeId, traits, Eval.now(schema))

private def updateWithHints[A](
shapeId: ShapeId,
hints: Hints,
lSchema: Schema[A]
): Unit = updateWithHints(shapeId, hints, Eval.now(lSchema))

def default: Unit = ()

override def integerShape(id: ShapeId, shape: IntegerShape): Unit =
Expand Down Expand Up @@ -256,9 +243,36 @@ private[dynamic] object Compiler {
)
}

val theEnum = stringEnumeration(values, values)
update(id, shape.traits, makeStringEnum(id, values, shape.traits))
}

private def makeStringEnum(
id: ShapeId,
values: List[EnumValue[Int]],
traits: Map[IdRef, Document]
) = {
if (traits.contains(IdRef("alloy#openEnum"))) {
// the runtime representation of normal enums is Int, but for open enums it's String to support arbitrary unknown values.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should we go for a String representation for all the cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose that'd make sense.

Copy link
Contributor

@Baccata Baccata Oct 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although, int is fine for the closed-enum case

val mappedValues = values.map(_.map(_.asLeft[String]))

enumeration[Either[Int, String]](
_.fold(
mappedValues,
unknownValueString =>
EnumValue(
stringValue = unknownValueString,
intValue = -1,
value = unknownValueString.asRight[Int],
name = "$Unknown",
hints = Hints.empty
)
),
EnumTag.OpenStringEnum(_.asRight[Int]),
mappedValues
)
} else
stringEnumeration(values, values)

update(id, shape.traits, theEnum)
}

override def intEnumShape(id: ShapeId, shape: IntEnumShape): Unit = {
Expand Down Expand Up @@ -289,14 +303,31 @@ private[dynamic] object Compiler {

val valueList = values.map(_._2).toList.sortBy(_.intValue)

val theEnum = intEnumeration(values.apply, valueList)
if (shape.traits.contains(IdRef("alloy#openEnum"))) {
val theEnum = enumeration[Int](
v =>
values.getOrElse(
v,
EnumValue(
stringValue = v.toString,
intValue = v,
value = v,
name = "$Unknown",
hints = Hints.empty
)
),
EnumTag.OpenIntEnum(identity),
valueList
)

updateWithHints(
id, {
allHints(shape.traits)
},
theEnum
)
update(id, shape.traits, theEnum)
} else {
update(
id,
shape.traits,
intEnumeration(values, valueList)
)
}
}

kubukoz marked this conversation as resolved.
Show resolved Hide resolved
override def booleanShape(id: ShapeId, shape: BooleanShape): Unit =
Expand Down Expand Up @@ -325,11 +356,10 @@ private[dynamic] object Compiler {
hints = Hints.empty
)
}
val fromOrdinal = values(_: Int)
update(
id,
shape.traits,
enumeration(fromOrdinal, EnumTag.ClosedStringEnum, values)
makeStringEnum(id, values, shape.traits)
)
}
case _ => update(id, shape.traits, string)
Expand Down
64 changes: 62 additions & 2 deletions modules/dynamic/test/src-jvm/smithy4s/dynamic/EnumSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package smithy4s.dynamic

import munit.FunSuite
import smithy4s.ShapeId
import smithy4s.schema.Schema.EnumerationSchema
import munit.Location
Expand All @@ -25,7 +24,7 @@ import smithy4s.Hints
import smithy4s.Document
import smithy4s.schema.Schema

class EnumSpec extends FunSuite {
class EnumSpec extends DummyIO.Suite {
val model = """
$version: "2"
namespace example
Expand Down Expand Up @@ -61,6 +60,25 @@ class EnumSpec extends FunSuite {
@deprecated ICE,
FIRE
}

@alloy#openEnum
enum OpenStringEnum {
ICE,
FIRE
}

@alloy#openEnum
intEnum OpenIntEnum {
ICE = 42,
FIRE = 10
}

@enum([
{ value: "Vanilla" },
{ value: "Ice" }
])
@alloy#openEnum
string Open10Enum
"""

val compiled = Utils.compile(model)
Expand Down Expand Up @@ -230,4 +248,46 @@ class EnumSpec extends FunSuite {
)
)
}

test("Smithy 2.0 open string enums can be decoded to UNKNOWN") {
compiled.map { index =>
val schema = index
.getSchema(ShapeId("example", "OpenStringEnum"))
.getOrElse(fail("Error: shape missing"))

decodeEncodeCheck(schema)(Document.fromString("not a known value"))
}
}

test("Smithy 2.0 open int enums can be decoded to UNKNOWN") {
compiled.map { index =>
val schema = index
.getSchema(ShapeId("example", "OpenIntEnum"))
.getOrElse(fail("Error: shape missing"))

decodeEncodeCheck(schema)(Document.fromInt(52))
}
}

test("Smithy 1.0 open string enums can be decoded to UNKNOWN") {
compiled.map { index =>
val schema = index
.getSchema(ShapeId("example", "Open10Enum"))
.getOrElse(fail("Error: shape missing"))

decodeEncodeCheck(schema)(Document.fromString("not a known value"))
}
}

private def decodeEncodeCheck[A](schema: Schema[A])(input: Document) = {
val decoded = Document.Decoder
.fromSchema(schema)
.decode(input)
.toTry
.get

val encoded = Document.Encoder.fromSchema(schema).encode(decoded)

assertEquals(encoded, input)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ private[dynamic] trait PlatformUtils { self: Utils.type =>
SModel
.assembler()
.addUnparsedModel("dynamic.smithy", string)
.addImport(
// Alloy open enums
getClass().getClassLoader.getResource("META-INF/smithy/enums.smithy")
)
.assemble()
.unwrap()
)
Expand Down