Last active
September 30, 2021 10:36
-
-
Save rbuckland/11229137 to your computer and use it in GitHub Desktop.
Sometimes you just want to call the copy method for the same "parameters", but not care about what underlying type it is.Because case class .copy() methods are auto generated per case class, you can't call copy on the abstract class, so this approach uses some scala foo to generate a scala Function that generates the correct magic for you.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.soqqo.luap.messages | |
import com.soqqo.luap.messages.ContextMetaData | |
import com.soqqo.luap.messages.NoContextMetaData | |
import com.soqqo.luap.messages.RegisterNewPerson | |
import io.straight.fw.model.Uuid | |
import scala.reflect.api.JavaUniverse | |
import scala.reflect.api | |
import io.straight.fw.jackson.JacksonBindingSupport._ | |
import scala.Some | |
/** | |
* @author rbuckland | |
*/ | |
abstract class Message(val timestamp: Long, val meta: MetaData) extends Serializable | |
abstract class EventMessage(override val timestamp: Long, override val meta: MetaData) extends Message(timestamp, meta) | |
abstract class CommandMessage(override val timestamp: Long, override val meta: MetaData) extends Message(timestamp, meta) | |
sealed abstract class MetaData | |
case class NoContextMetaData() extends MetaData | |
case class ContextMetaData(accountId: Option[Uuid], userId: Option[Uuid], clientHost: Option[String]) extends MetaData | |
/* | |
* Build a "sealed abstract class "copy()" function | |
*/ | |
object MessageSupport { | |
val cm = scala.reflect.runtime.currentMirror | |
import cm.universe._ | |
private def buildFunction(theType: Type, subs: List[Symbol]): Function = { | |
object copierNme { | |
val x: TermName = "x" | |
val base: TermName = "base" | |
val meta: TermName = "meta" | |
val newMeta: TermName = "newMeta" | |
val timestamp: TermName = "timestamp" | |
val newTimestamp: TermName = "newTimestamp" | |
val copy: TermName = "copy" | |
} | |
/** | |
* This method creates a "Case defintiiaon .. " like | |
* | |
* <pre> | |
* ((base: O.Base, newId: Int) => base match { | |
* case (x @ (_: Bar)) => x.copy(timestamp = newTimestamp, meta = mewMeta) | |
* case (x @ (_: Foo)) => x.copy(timestamp = newTimestamp, meta = mewMeta) | |
* }) | |
* @param subClass | |
* @return | |
*/ | |
def mkCase(subClass: Symbol):CaseDef = { | |
val bind = Bind(copierNme.x, Typed(Ident(nme.WILDCARD), Ident(subClass))) | |
val copyApply = Apply(Select(Ident(copierNme.x), copierNme.copy), List( | |
AssignOrNamedArg(Ident(copierNme.timestamp), Ident(copierNme.newTimestamp)), | |
AssignOrNamedArg(Ident(copierNme.meta), Ident(copierNme.newMeta)) | |
)) | |
CaseDef(bind, EmptyTree, copyApply) | |
} | |
val param1 = ValDef(Modifiers(Flag.PARAM), copierNme.base, TypeTree(theType), EmptyTree) | |
val param2 = ValDef(Modifiers(Flag.PARAM), copierNme.newTimestamp, TypeTree(typeOf[Long]), EmptyTree) | |
val param3 = ValDef(Modifiers(Flag.PARAM), copierNme.newMeta, TypeTree(typeOf[MetaData]), EmptyTree) | |
Function(List(param1, param2, param3), Match(Ident(copierNme.base), subs.toList.map(x => mkCase(x)))) | |
} | |
/** | |
* Build a function - called like this.. | |
* <pre> | |
* val campusCopier = MessageSupport.makeCopier[CampusCommand] | |
* val newCmd = campusCopier(cmd,newTimestampValue,newMetaData) | |
* <pre> | |
* @param t | |
* @tparam M | |
* @return | |
*/ | |
def makeCopier[M](implicit t: TypeTag[M]) = { | |
import scala.tools.reflect.ToolBox | |
val tb = cm.mkToolBox() | |
val base = typeOf[M] | |
val subs = base.typeSymbol.asClass.knownDirectSubclasses // find all "sealed" case classes from the abstract class parent | |
tb.compile(buildFunction(base, subs.toList))().asInstanceOf[((M, Long, MetaData) => M)] | |
} | |
} | |
/* | |
* file: MessageEnricher.scala | |
*/ | |
package com.soqqo.luap.messages | |
import java.util.Date | |
import io.straight.fw.model.Uuid | |
/** | |
* The Message Enricher will call the case class Copy Method for the relevant underlying class | |
* taking in the timestamp and the MetaData entry required to "enrich" into the Command | |
* | |
* This trick relies on a few facts. | |
* (1) the Macro compiler - see MessageSupport.makeCopier // courtesy of https://groups.google.com/forum/#!topic/scala-user/ro57WMdH5EY | |
* (2) the all subclasses for a type are sealed, the macro copier relies on this to generate a "case tree" | |
* | |
* @author rbuckland | |
*/ | |
object MessageEnricher { | |
val accountTenantCopier = MessageSupport.makeCopier[AccountTenantCommand] | |
val campusCopier = MessageSupport.makeCopier[CampusCommand] | |
val cCardCopier = MessageSupport.makeCopier[ConnectCardCommand] | |
val cgroupCopier = MessageSupport.makeCopier[ConnectGroupCommand] | |
val personCopier = MessageSupport.makeCopier[PersonCommand] | |
val rosterCopier = MessageSupport.makeCopier[RosterCommand] | |
def enrich[A <: Message](cmd: A,account:Uuid, user:Uuid, clientHost:Option[String]) = { | |
enrichInternal(cmd,ContextMetaData(Some(account),Some(user),clientHost)) | |
} | |
def enrich[A <: Message](cmd: A,account:Uuid, clientHost:Option[String]) = { | |
enrichInternal(cmd,ContextMetaData(Some(account),None,clientHost)) | |
} | |
def enrich[A <: Message](cmd: A,clientHost:Option[String]) = { | |
enrichInternal(cmd,ContextMetaData(None,None,clientHost)) | |
} | |
def enrich[A <: Message](cmd: A) = { | |
enrichInternal(cmd,NoContextMetaData()) | |
} | |
def enrichInternal[A <: Message](cmd: A,meta: MetaData) = { | |
val t:Long = new Date().getTime | |
cmd match { | |
// others | |
case x : ConnectCardCommand => cCardCopier(x,t,meta) | |
case x : PersonCommand => personCopier(x,t,meta) | |
// ... | |
case _ => throw new IllegalArgumentException("The Developer has forgotten to map the super class of " + cmd.getClass + " into this enricher") | |
} | |
} | |
} | |
/* | |
* The Message you are about to see .. | |
*/ | |
/* | |
Command message - browser sends us this | |
*/ | |
sealed abstract class ConnectCardCommand(@jsonIgnore override val timestamp: Long, | |
@jsonIgnore override val meta: MetaData) extends EventMessage(timestamp, meta) | |
case class SubmitConnectCardRegistration(override val timestamp: Long, | |
override val meta: MetaData, | |
accountTenant: Uuid, | |
queryTopics: List[String], | |
registerNewPerson: RegisterNewPerson, | |
onBehalfOf: Option[BasicContact], | |
campus: Uuid | |
) extends ConnectCardCommand(timestamp, meta) { | |
def this() = this(-1, null, null, Nil, null, None, null) // jackson needs this because it received the JSON on the wire and | |
// has trouble working out how to marshal a new one | |
} | |
/* | |
* Usage: in Spray | |
*/ | |
post { | |
entity(as[SubmitConnectCardRegistration]) { cmd => | |
clientIP { ip => complete { | |
val clientHost = ip.toOption.map(_.getHostAddress) | |
connectCardSaga.ask(MessageEnricher.enrich(cmd,account.id,clientHost)).mapTo[SZDomainValidation[ConnectCard]] | |
} } | |
} | |
} ~ | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment