Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbuckland/11229137 to your computer and use it in GitHub Desktop.
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.
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