What is the recommended way to implement a Codec Provider for a Trait which will choose the case class based on an attribute of the Trait?
Here is a short example:
I have the following Account Trait:
object AccountType extends Enumeration {
type AccountType = Value
val Administrator: Value = Value("administrator")
val User: Value = Value("user")
}
trait Account {
val id: ObjectId
val `type`: AccountType
val login: String
val groups: Option[Seq[Group]]
val certificate: Option[CFCertificate]
val creationDate: Instant
}
And I have two case classes extending the Account Trait:
case class UserAccount(
@BsonProperty("_id") id: ObjectId = new ObjectId(),
`type`: AccountType = AccountType.User,
login: String,
groups: Option[Seq[Group]] = None,
certificate: Option[CFCertificate],
creationDate: Instant = Instant.now()
) extends Account
object UserAccount {
implicit val userAccountCodecProvider: CodecProvider = Macros.createCodecProviderIgnoreNone[UserAccount]()
}
case class AdministratorAccount(
@BsonProperty("_id") id: ObjectId = new ObjectId(),
`type`: AccountType = AccountType.Administrator,
login: String,
groups: Option[Seq[Group]] = None,
certificate: Option[CFCertificate],
creationDate: Instant = Instant.now()
) extends Account
object AdministratorAccount {
implicit val administratorAccountCodecProvider: CodecProvider = Macros.createCodecProviderIgnoreNone[AdministratorAccount]()
}
In the companion object of the Account Trait, I created the following implicit Codec Provider:
object Account {
implicit val accountCodecProvider: CodecProvider = new CodecProvider {
override def get[T](clazz: Class[T], registry: CodecRegistry): Codec[T] = {
if (classOf[Account].isAssignableFrom(clazz)) {
new Codec[Account] {
override def encode(writer: BsonWriter, value: Account, encoderContext: EncoderContext): Unit = value match {
case administratorAccount: AdministratorAccount => registry.get[AdministratorAccount](classOf[AdministratorAccount]).encode(writer, administratorAccount, encoderContext)
case userAccount: UserAccount => registry.get[UserAccount](classOf[UserAccount]).encode(writer, userAccount, encoderContext)
case other => throw new CodecConfigurationException(s"Unsupported Account type ${other.`type`}")
}
override def getEncoderClass: Class[Account] = classOf[Account]
override def decode(reader: BsonReader, decoderContext: DecoderContext): Account = {
val document = decoderContext.decodeWithChildContext(new BsonDocumentCodec(), reader)
val accountType = document.getString("type").getValue
AccountType.withName(accountType) match {
case AccountType.Administrator => registry.get[AdministratorAccount](classOf[AdministratorAccount]).decode(new BsonDocumentReader(document), decoderContext)
case AccountType.User => registry.get[UserAccount](classOf[UserAccount]).decode(new BsonDocumentReader(document), decoderContext)
case other => throw new CodecConfigurationException(s"Unsupported Account type $other")
}
}
}.asInstanceOf[Codec[T]]
} else
null
}
}
}
Is this the recommended / idiomatic way to do so?
Furthermore, and as requested in the ticket Need architecture guidance with Codecs and Codec Providers, is there a way to avoid declaring all the codecs in a local registry in my Service class which manage Accounts (CRUD) but load them implicitly when required:
@Singleton
class AccountService @Inject() (implicit ec: ExecutionContext, mongoDbApi: MongoDbApi){
private val codecRegistry: CodecRegistry = fromRegistries(
fromCodecs(CFCertificateCodec),
fromProviders(
AccountType.accountTypeCodecProvider,
Group.groupCodecProvider,
AdministratorAccount.administratorAccountCodecProvider,
UserAccount.userAccountCodecProvider,
Account.accountCodecProvider
),
DEFAULT_CODEC_REGISTRY
)