-
-
Notifications
You must be signed in to change notification settings - Fork 99
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
JsonCodecMaker fails in self-type scenario #1188
Comments
@nkgm Thanks for the question! What are you going to do with that codecs? Could you please update your code snippet with lines for creation of Which JSON representation are you expecting for them? |
Hey @plokhotnyuk, thanks for looking! This is still largely experimental, so feel free to poke holes in it if you must :) It's a micro framework for DDD/ES that attempts to eliminate as much boilerplate as possible by use of Scala3 macro annotations ( I tried to keep it as short as possible without leaving out key bits of the motivation behind it. I have added I'm still figuring out a few things, so implementation will be incomplete. Framework codetype AggregateID[AT <: Aggregate] = FlakeID @@ AT
trait AggregateState[AT <: Aggregate]
trait AggregateRoot[AT <: Aggregate] {
def version: AggregateVersion[AT]
def props: Aggregate.GetIdProps[AT]
}
trait AggregateVersion[AT <: Aggregate] {
def aid: Aggregate.GetID[AT]
def eid: FlakeID
}
type JsonString = String
type EventName = String
trait Aggregate { self =>
type ID = FlakeID @@ self.type
inline given (using ev: JsonValueCodec[FlakeID]): JsonValueCodec[ID] = ev.asInstanceOf[JsonValueCodec[ID]]
type Props[F[_]]
type EntityProps = Props[Id]
type OptionProps = Props[Option]
case class Entity(version: EntityVersion, props: EntityProps) extends AggregateRoot[self.type]
case class EntityVersion(aid: ID, eid: FlakeID) extends AggregateVersion[self.type]
trait EntityState extends AggregateState[self.type]
trait EntityEvent extends AggregateEvent[self.type]
case object Initial extends EntityState
inline def eventName[E <: EntityEvent]: EventName = ${eventNameImpl}
// autogenerated by @aggregate **SERIALIZATION**
def encodeEvent[E <: EntityEvent](event: E): (EventName, JsonString)
// autogenerated by @aggregate **PARSING**
def decodeEvent(eventName: EventName, json: JsonString): EntityEvent
// autogenerated by @aggregate
def updateProps(props: EntityProps, optionProps: OptionProps): EntityProps
// autogenerated by @aggregate
def handleEvents(state: EntityState, event: EntityEvent): UnionStates
def processEvent(event: UnionEvents, state: Option[UnionStates] = None): UnionStates =
handleEvents(state.getOrElse(Initial), event)
def processEvents(events: List[UnionEvents], state: Option[UnionStates] = None): UnionStates =
events.foldLeft(processEvent(events.head, state))((s, e) => handleEvents(s, e))
type UnionStates <: EntityState
type UnionEvents <: EntityEvent
// autogenerated by @aggregate
def entityPropsJsonCodec: JsonValueCodec[EntityProps]
def optionPropsJsonCodec: JsonValueCodec[OptionProps]
val entityVersionJsonCodec: JsonValueCodec[EntityVersion] =
JsonCodecMaker.make[EntityVersion]
inline given JsonValueCodec[EntityProps] = entityPropsJsonCodec
inline given JsonValueCodec[OptionProps] = optionPropsJsonCodec
inline given JsonValueCodec[EntityVersion] = entityVersionJsonCodec
inline given self.type = self
}
trait AggregateEvent[AT <: Aggregate]
trait CrudEvents { self: Aggregate =>
type CrudUnionStates = Initial.type | Active | Inactive
type CrudUnionEvents = CreatedEvent | UpdatedEvent | DeletedEvent.type
case class Active(props: EntityProps) extends EntityState
case class Inactive(props: EntityProps) extends EntityState
case class CreatedEvent(props: EntityProps) extends EntityEvent
case class UpdatedEvent(optionProps: OptionProps) extends EntityEvent
case object DeletedEvent extends EntityEvent
// **ISSUE** assume I had to let @aggregate generate this for me for whatever reason
// given createdEventCodec: JsonValueCodec[CreatedEvent] = JsonCodecMaker.make[CreatedEvent]
given updatedEventCodec: JsonValueCodec[UpdatedEvent] = JsonCodecMaker.make[UpdatedEvent]
given deletedEventCodec: JsonValueCodec[DeletedEvent.type] = JsonCodecMaker.make[DeletedEvent.type]
def handleEvent(state: Initial.type, event: CreatedEvent) = Active(event.props)
def handleEvent(state: Active, event: UpdatedEvent) = Active(updateProps(state.props, event.optionProps))
def handleEvent(state: Active, event: DeletedEvent.type) = Inactive(state.props)
} User code@aggregate
object Person extends Aggregate with CrudEvents {
// Higher-Kinded Data FTW!
case class Props[F[_]](name: F[String], age: F[Int])
object Props {
// Props("John", 33) gives Props[Id]("John", 33)
// Props[Option]("John".some) gives Props[Option]("John", None)
// Any F[_] with a MonoidK instance gets auto default params
// If no MonoidK and params omitted -> compiletime error
inline def apply[F[_]](using Infer[F])(
inline name: F[String] = autop[F[String]],
inline age: F[Int] = autop[F[Int]]
): Props[F] = new Props[F](name, age)
}
type UnionStates = CrudUnionStates
type UnionEvents = CrudUnionEvents | MyEvent
case class MyEvent(name: String) extends EntityEvent
def handleEvent(state: EntityState, event: MyEvent) = ???
//////////////////////// AUTOGENERATED BY @aggregate ////////////////////////
val entityPropsJsonCodec: JsonValueCodec[EntityProps] = JsonCodecMaker.make
val optionPropsJsonCodec: JsonValueCodec[OptionProps] = JsonCodecMaker.make
// only generate codecs that can't be found in scope
val _myEventCodec: JsonValueCodec[MyEvent] = JsonCodecMaker.make
// Trying to generate the codec for `CrudEvents.CreatedEvent` here will fail
// This is the very reason for this **ISSUE**
val _createdEventCodec: JsonValueCodec[CreatedEvent] = JsonCodecMaker.make
// No implicit 'com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec[_ >: scala.Nothing <: scala.Any]'
// defined for 'CrudEvents.this.Props[cats.Id]'.
// codec already found in `CrudEvents` mixin, so macro will use that instead
// val _updatedEventCodec: JsonValueCodec[UpdatedEvent] = JsonCodecMaker.make
// @aggregate will first check for the existence of `handleEvent(s <: EntityState, e in UnionEvents)`
// for all UnionEvents and give a compile-time error if any events go unhandled.
// nb: the implementation auto-sorts cases by specificity so the more specific ones come first
def handleEvents(state: EntityState, event: EntityEvent): UnionStates =
(state, event) match {
case (s: Initial.type, e: CreatedEvent) => handleEvent(s, e)
case (s: Active, e: UpdatedEvent) => handleEvent(s, e)
case (s: Active, e: DeletedEvent.type) => handleEvent(s, e)
case (s: EntityState, e: MyEvent) => handleEvent(s, e)
case (s: EntityState, e: PersonEvent) => handleEvent(s, e)
case default$macro$1 =>
throw IllegalStateException(
StringBuilder("No handler for (")
.append(state)
.append(", ")
.append(event)
.append(")")
.toString()
)
}
def updateProps(props: EntityProps, optionProps: OptionProps): EntityProps = props.patchUsing(optionProps)
// **SERIALIZATION**
def encodeEvent[E <: EntityEvent](event: E): (EventName, JsonString) = event match
case e: CreatedEvent => ("CreatedEvent", writeToString(e)(using _createdEventCodec))
case e: UpdatedEvent => ("UpdatedEvent", writeToString(e)(using updatedEventCodec)) // codec can reside in any of the mixed in traits
case e: MyEvent => ("MyEvent", writeToString(e)(using _myEventCodec))
case whatever => ???
// **PARSING**
def decodeEvent(eventName: EventName, json: JsonString): EntityEvent = eventName match
case "CreatedEvent" => readFromString(json)(using _createdEventCodec)
case "UpdatedEvent" => readFromString(json)(using updatedEventCodec) // codec can reside in any of the mixed in traits
case "MyEvent" => readFromString(json)(using _myEventCodec)
case whatever => ???
////////////////////////////////////////////////////////////////////////////////////////////////////////
}
|
Hey @plokhotnyuk, I think I found a fix but it would require a refactoring of |
@nkgm Thanks for the help! I cannot promise that your refactoring will be merged as is. Mostly because |
Hey @plokhotnyuk , fully agree. Since I'll need to use the new experimental codec maker alongside What do you think? |
Here's a simplified scenario. Any insight would be greatly appreciated.
The text was updated successfully, but these errors were encountered: