SUGGESTION: Scala Union -> ADT code generation custom names
There are 2 things I would like to see smithy4s supports:
- Being able to create an 2 ADTs with similar schemas
- Being able to create a union with actual case objects when structure contains empty fields
Smithy spec
@adt
union CustomerAction {
@unionEntityName("Buy")
buy: CustomerBuy
}
@adt
union SellerAction {
@unionEntityName("Buy")
buy: SellerBuy
removeSellAccount: RemoveSellAccount
}
structure CustomerBuy {
quantity: Integer
}
structure SellerBuy {
quantity: Integer
}
structure RemoveSellAccount {} // <- This will generate a case class instead of case object.
The reason why those features will be nice to see, its for the 1 to 1 transformations you can get for free with libraries like chimney to your domain after request passed validation.
What's unionEntityName here?
I'm not sure if this would be such a significant improvement to have case objects just for the sake of easier conversions, but I do see the possible performance optimization in not having to instantiate empty case classes over and over (new RemoveSellAccount on every decode). ~~This might be worth special-casing in the StructureSchema constructors (using a singleton instance of the case class).~~ Looks like we already special-case that:
case class Hello()
object Hello extends ShapeTag.Companion[Hello] {
val id: ShapeId = ShapeId("test", "Hello")
val hints: Hints = Hints.empty
implicit val schema: Schema[Hello] = constant(Hello()).withId(id).addHints(hints)
}
btw. to make a better case, it would be useful to see a concrete example of what your desired Scala code would look like.
Sorry If I wasn't clear, the @unionEntityName trait doesn't exist its a suggestion. The limitation I see here is related to smithy and not necessarily the targeted language. Its possible to have 2 ADTs that are describing very similar behaviours of users but slightly different, often they can share more than one common data structure like in the example bellow its just the Buy action. But imagine a case where are the shared common structures are more. The practise most of the times I follow is to validate the request and transform the request to your domain. Naming my internal domain to be 1 to 1 with what smithy can generate I would like to avoid. A more detailed example is bellow:
Smithy spec
@adt
union ClientAction { // <- A client action to describe all actions user can take, if he did an action as a seller or as customer
customerAction: CustomerAction
sellerAction: SellerAction
}
@adt
union CustomerAction {
buy: CustomerBuy
}
@adt
union SellerAction {
buy: SellerBuy
removeSellAccount: RemoveSellAccount
}
structure CustomerBuy {
quantity: Integer
}
structure SellerBuy {
quantity: Integer
}
structure RemoveSellAccount {} // <- This will generate a case class instead of case object.
Generated CustomerAction
sealed trait CustomerAction extends scala.Product with scala.Serializable {
@inline final def widen: CustomerAction = this
}
object CustomerAction extends ShapeTag.Companion[CustomerAction] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "CustomerAction")
val hints: Hints = Hints.empty
case class CustomerBuy(quantity: Option[Int] = None) extends CustomerAction
object CustomerBuy extends ShapeTag.Companion[CustomerBuy] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "CustomerBuy")
val hints: Hints = Hints.empty
val schema: Schema[CustomerBuy] = struct(
int.optional[CustomerBuy]("quantity", _.quantity),
){
CustomerBuy.apply
}.withId(id).addHints(hints)
val alt = schema.oneOf[CustomerAction]("buy")
}
implicit val schema: Schema[CustomerAction] = union(
CustomerBuy.alt,
){
case c: CustomerBuy => CustomerBuy.alt(c)
}.withId(id).addHints(hints)
}
Generated SellerAction
sealed trait SellerAction extends scala.Product with scala.Serializable {
@inline final def widen: SellerAction = this
}
object SellerAction extends ShapeTag.Companion[SellerAction] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "SellerAction")
val hints: Hints = Hints.empty
case class SellerBuy(quantity: Option[Int] = None) extends SellerAction
object SellerBuy extends ShapeTag.Companion[SellerBuy] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "SellerBuy")
val hints: Hints = Hints.empty
val schema: Schema[SellerBuy] = struct(
int.optional[SellerBuy]("quantity", _.quantity),
){
SellerBuy.apply
}.withId(id).addHints(hints)
val alt = schema.oneOf[SellerAction]("buy")
}
case class RemoveSellAccount() extends SellerAction
object RemoveSellAccount extends ShapeTag.Companion[RemoveSellAccount] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "RemoveSellAccount")
val hints: Hints = Hints.empty
implicit val schema: Schema[RemoveSellAccount] = constant(RemoveSellAccount()).withId(id).addHints(hints)
val alt = schema.oneOf[SellerAction]("removeSellAccount")
}
implicit val schema: Schema[SellerAction] = union(
SellerBuy.alt,
RemoveSellAccount.alt,
){
case c: SellerBuy => SellerBuy.alt(c)
case c: RemoveSellAccount => RemoveSellAccount.alt(c)
}.withId(id).addHints(hints)
}
Desired CustomerAction
sealed trait CustomerAction extends scala.Product with scala.Serializable {
@inline final def widen: CustomerAction = this
}
object CustomerAction extends ShapeTag.Companion[CustomerAction] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "CustomerAction")
val hints: Hints = Hints.empty
case class Buy(quantity: Option[Int] = None) extends CustomerAction // <- Notice the name of the class here
object Buy extends ShapeTag.Companion[Buy] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "Buy")
val hints: Hints = Hints.empty
val schema: Schema[Buy] = struct(
int.optional[Buy]("quantity", _.quantity),
){
Buy.apply
}.withId(id).addHints(hints)
val alt = schema.oneOf[CustomerAction]("buy")
}
implicit val schema: Schema[CustomerAction] = union(
Buy.alt,
){
case c: Buy => Buy.alt(c)
}.withId(id).addHints(hints)
}
Desired CustomerAction
sealed trait SellerAction extends scala.Product with scala.Serializable {
@inline final def widen: SellerAction = this
}
object SellerAction extends ShapeTag.Companion[SellerAction] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "SellerAction")
val hints: Hints = Hints.empty
case class Buy(quantity: Option[Int] = None) extends SellerAction // <- Notice the name of the class here
object Buy extends ShapeTag.Companion[Buy] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "Buy")
val hints: Hints = Hints.empty
val schema: Schema[Buy] = struct(
int.optional[Buy]("quantity", _.quantity),
){
Buy.apply
}.withId(id).addHints(hints)
val alt = schema.oneOf[SellerAction]("buy")
}
case class RemoveSellAccount() extends SellerAction
object RemoveSellAccount extends ShapeTag.Companion[RemoveSellAccount] {
val id: ShapeId = ShapeId("com.pirum.fors.event_gateway.user_action.http", "RemoveSellAccount")
val hints: Hints = Hints.empty
implicit val schema: Schema[RemoveSellAccount] = constant(RemoveSellAccount()).withId(id).addHints(hints)
val alt = schema.oneOf[SellerAction]("removeSellAccount")
}
implicit val schema: Schema[SellerAction] = union(
Buy.alt,
RemoveSellAccount.alt,
){
case c: Buy => Buy.alt(c)
case c: RemoveSellAccount => RemoveSellAccount.alt(c)
}.withId(id).addHints(hints)
}
Naming my internal domain to be 1 to 1 with what smithy can generate I would like to avoid.
Maybe a preprocessor is better suited , https://disneystreaming.github.io/smithy4s/docs/guides/model-preprocessing
Naming my internal domain to be 1 to 1 with what smithy can generate I would like to avoid.
Maybe a preprocessor is better suited , https://disneystreaming.github.io/smithy4s/docs/guides/model-preprocessing
Not sure if this is going to solve my problem here or is going to make it any easier since the transformation needs to be written explicitly.
This issue should be split in two that could be handled separately :
- Generate empty structures as case objects (whether they're used in or out of unions)
- Allow for customising names of ADT members
Regarding 1 :
In the current state, Smithy4s will render as case objects union members that target the Unit shape provided by the Smithy standard library. This means you don't have to create empty structures yourself :
@adt
union SellerAction {
removeSellAccount: Unit
}
Regarding 2 :
I'm not fundamentally opposed to it, provided it's called @generatedName and constrained (via smithy selectors) union-members. A more generic name would open the door to re-using the same trait in other usecases later on.