no.kodeworks.kvarg.repo.Repos.scala Maven / Gradle / Ivy
package no.kodeworks.kvarg.repo
import akka.actor.Actor.Receive
import akka.actor.{Actor, ActorLogging, ActorRef}
import akka.event.{LoggingAdapter, NoLogging}
import akka.stream.{Materializer, OverflowStrategy}
import akka.stream.scaladsl.{Keep, Sink, Source, SourceQueueWithComplete}
import akka.util.Timeout
import cats.syntax.option._
import no.kodeworks.kvarg.actor.CometActor.CometSend
import no.kodeworks.kvarg.actor.DbService._
import no.kodeworks.kvarg.message._
import no.kodeworks.kvarg.model.HasId
import no.kodeworks.kvarg.util.IdGen
import no.kodeworks.kvarg.check.{Check, Checked}
import no.kodeworks.kvarg.check.syntax._
import no.kodeworks.kvarg.patch
import no.kodeworks.kvarg.patch.{Patch, Pvalue}
import no.kodeworks.kvarg.refs.{RefLookup, Refs}
import shapeless.ops.hlist.{Fill, Length, LiftAll, Mapped, Mapper, Zip}
import shapeless.{::, HList, HNil, Nat, Poly1, Strict, Typeable}
import no.kodeworks.kvarg.util._
import scala.collection.mutable
import scala.concurrent.Future
sealed trait ServiceRepos {
def init(initSuccess: Option[() => Unit] = None): Unit
def initing(initSuccess: Option[() => Unit] = None): Receive
def receive(): Receive
}
sealed trait Repos[Domain <: HList] {
type RepoDomain <: HList
def serviceRepos
(
actor: Actor with ActorLogging,
timeout: Timeout,
mat: Materializer,
refLookups: collection.Map[String, RefLookup[_, _]]
): ServiceRepos
def repoMap: Map[String, Repo[_]]
def repos: RepoDomain
def get[T](g: Get[T]): GetReply =
repoMap.get(typeableToSimpleName(g.tt)).map { repo0 =>
repo0.get(g.asInstanceOf[Get[repo0.Model0]])
}.getOrElse(GetBadType)
def save(s: Save[_], r: Refs[_]): SaveReply[_] =
repoMap.get(typeableToSimpleName(s.tt)).map { repo0 =>
repo0.save(s.asInstanceOf[Save[repo0.Model0]], r.asInstanceOf[Refs[repo0.Model0]])
}.getOrElse(SaveBadType(Nil))
def update(u: Update[_], r: Refs[_]): SaveReply[_] = {
repoMap.get(typeableToSimpleName(u.tt)).map { repo0 =>
repo0.update(u.asInstanceOf[Update[repo0.Model0]], r.asInstanceOf[Refs[repo0.Model0]])
}.getOrElse(SaveBadType(Nil))
}
}
final case class Repo[Model]
(
check: Check[Model],
log: LoggingAdapter = NoLogging
)
(implicit
hasId0: HasId[Model],
typeable: Typeable[Model],
patcher: patch.Patcher[Model]
) {
type Model0 = Model
val idGen = new IdGen
val completes: mutable.Map[Long, Model] = mutable.Map()
val patches: mutable.Map[Long, Patch[Model]] = mutable.Map()
log.info(s"Created repo for ${typeable.describe}")
def hasId: HasId[Model0] = hasId0
def get(g: Get[Model0]): GetReply = {
log.debug(s"Get {}", g)
g match {
case Get(Some(id)) =>
(completes.get(id), patches.get(id)) match {
case (Some(p), Some(pp)) =>
GetReplyPatch(pp(Patch.of(p)(pp.patcher)))
case (_, Some(pp)) => GetReplyPatch(pp)
case (Some(p), _) => GetReplyComplete(p)
case (_, _) => GetReplyEmpty
}
case _ => GetReplyList(completes.values.toList)
}
}
def ref(pp: Patch[Model0]): Future[Refs[Model0]] = ???
def ref(p: Model0): Future[Refs[Model0]] = ???
def save(s: Save[Model0], r: Refs[Model0]): SaveReply[Model0] = {
log.info(s"Save ${typeable.describe}")
val checkeds = check(s.t) ++ check(r)
def invalid = checkeds.exists(!_.valid)
def id = hasId.id(s.t).get
def toPatch = Patch.of(s.t)
if (invalid) {
log.debug("invalid complete, attempting store as patch")
update(Update(Patch.of(s.t), s.page, s.commit, s.stash), r)
} else if (hasId.id(s.t).isEmpty) {
val newId = idGen.getAndIncId
log.debug(s"valid complete without id, storing with new id $newId")
val inserted = hasId.withId(s.t, Some(newId))
completes += newId -> inserted
val toPatch = Patch.of(inserted)
SaveInserted(inserted, toPatch, checkeds)
} else if (completes.contains(id)) {
log.debug(s"valid complete with id $id, existing complete already stored, storing over existing")
val diff = toPatch.diff(Patch.of(completes(id)))
patches -= id
completes(id) = s.t
SaveUpdated(s.t, diff, checkeds)
} else if (patches.contains(id)) {
log.debug(s"valid complete with id $id, existing patch already stored, removing patch and storing")
val diff = toPatch.diff(patches(id))
patches -= id
completes(id) = s.t
SaveUpdated(s.t, diff, checkeds)
} else {
log.warning(s"valid complete with id, no existing complete with same id, not storing")
SaveBadType(checkeds).asInstanceOf[SaveReply[Model0]]
}
// Probably a bad idea, we need to use our id-generator:
// else {
// completes += hasId.id(s.t).get -> s.t
// SaveInserted(s.t)(hasId, typeable)
// }
}
def update(u: Update[Model0], r: Refs[Model0]): SaveReply[Model0] = {
log.info(s"Update ${typeable.describe}")
val checkeds = check(u.patch) ++ check(r)
val defaults = patcher.defaults
u.patch.get[Option[Long]]('id) match {
case Some(Some(id)) =>
def stashOrApply(pp: Patch[Model0], diff: Patch[Model0], checkeds0: List[Checked] = checkeds) = {
if (u.stash) {
log.debug("patch stashed")
UpdateStashed(pp, diff, checkeds0)
} else {
log.debug("storing as patch")
patches(id) = pp
IncompletelyUpdated(pp, diff, checkeds0)
}
}
(completes.get(id), patches.get(id)) match {
case (Some(p), Some(pp)) =>
log.debug("patch with id found in completes and patches")
val pp0 = u.patch(pp)
val diff = u.patch.diff(pp)
if (checkeds.exists(!_.valid)) {
log.debug("invalid patch")
stashOrApply(pp0, diff)
} else {
log.debug("valid patch, merging with existing patch, revalidate")
val checkeds0 = check(pp0)
if (checkeds0.exists(!_.valid)) {
log.debug("invalid patch")
stashOrApply(pp0, diff)
} else {
log.debug("patch and existing patch valid, merging with complete")
if (u.commit) {
val Some(p0) = pp0(p)()
patches -= id
completes(id) = p0
SaveUpdated(p0, diff, checkeds)
} else {
log.debug("patch not commited, trying to retain merged patch")
stashOrApply(pp0, diff, checkeds0)
}
}
}
case (_, Some(pp)) =>
log.debug("patch with id found in patches but not in completes")
val pp0 = u.patch(pp)
val diff = u.patch.diff(pp)
if (checkeds.exists(!_.valid)) {
log.debug("invalid patch")
stashOrApply(pp0, diff)
} else {
log.debug("valid patch, merging with existing patch, revalidate")
val checkeds0 = check(pp0, u.commit)
if (!checkeds0.exists(!_.valid)) {
log.debug("patch and existing patch valid, trying to build")
pp0() match {
case Some(p) =>
log.debug("patch built")
if (u.commit) {
log.debug("patch commited, removing exiting patch and inserting complete")
patches -= id
completes(id) = p
SaveInserted(p, diff, checkeds)
} else {
log.debug("patch not commited, trying to retain merged patch")
stashOrApply(pp0, diff, checkeds0)
}
case _ =>
log.debug("patch build failed, trying to update patches with merged patch")
stashOrApply(pp0, diff, checkeds0)
}
} else {
log.debug("invalid patch")
stashOrApply(pp0, diff, checkeds0)
}
}
case (Some(p), _) =>
log.debug("patch with id found in completes but not in patches")
val diff = u.patch.diff(Patch.of(p))
if (checkeds.exists(!_.valid)) {
log.debug("invalid patch")
stashOrApply(u.patch, diff)
} else {
log.debug("valid patch")
if (u.commit) {
log.debug("applying to complete instance")
val Some(p0) = u.patch(p)()
completes(id) = p0
SaveUpdated(p0, diff, checkeds)
} else {
log.debug("patch not commited")
stashOrApply(u.patch, diff)
}
}
case (_, _) =>
log.warning("patch with id not found in completes or patches")
SaveBadType(checkeds).asInstanceOf[SaveReply[Model0]]
}
case _ =>
log.debug("update without id: " + u.patch)
val withDefaults = u.patch(defaults)
log.debug("withDefaults: " + withDefaults)
def doPatch(checkeds: List[Checked]) = {
val newId: Long = idGen.getAndIncId
val withId = withDefaults.copy(withDefaults.poptions + ('id -> Pvalue(newId.some)))
patches(newId) = withId
IncompletelyUpdated(withId, withId, checkeds)
}
if (checkeds.exists(!_.valid)) {
log.debug("patch without id invalid")
if (u.stash) {
log.debug("patch stashed")
UpdateStashed(withDefaults, withDefaults, checkeds)
} else {
doPatch(checkeds)
}
} else withDefaults() match {
case Some(patchable) =>
log.debug("patch without id complete")
if (u.commit) {
log.debug("patch commited")
save(Save(patchable, u.page, u.commit, u.stash)(typeable), r)
} else {
doPatch(checkeds)
}
case _ =>
log.debug("patch without id incomplete")
val checkedAlls = check(u.patch, u.commit) ++ check(r)
if (u.stash) {
log.debug("patch stashed")
UpdateStashed(withDefaults, withDefaults, checkedAlls)
} else {
doPatch(checkedAlls)
}
}
}
}
}
object Repos {
type Aux[
Domain <: HList,
RepoDomain0 <: HList
] = Repos[Domain] {
type RepoDomain = RepoDomain0
}
final class MkRepos[Domain <: HList](
bootService: ActorRef,
dbService: ActorRef,
cometService: ActorRef,
log: LoggingAdapter) {
def apply[
PatchBuilderDomain <: HList,
CheckDomain <: HList,
CheckLength <: Nat,
LoggingAdapters <: HList,
CheckLogPatchBuilders <: HList,
RepoDomain0 <: HList,
Keys <: HList,
RepoGetAll <: HList
]
(checks: CheckDomain)
(implicit
patchers: Strict[LiftAll.Aux[patch.Patcher, Domain, PatchBuilderDomain]] = null
, repoMapped: Mapped.Aux[Domain, Repo, RepoDomain0]
, checkSize: Length.Aux[CheckDomain, CheckLength]
, loggingAdapters: Fill.Aux[CheckLength, LoggingAdapter, LoggingAdapters]
, checkLogPatchBuilder: Zip.Aux[CheckDomain :: LoggingAdapters :: PatchBuilderDomain :: HNil, CheckLogPatchBuilders]
, checksToRepoDomain: Mapper.Aux[mkRepo.type, CheckLogPatchBuilders, RepoDomain0]
, keys: TypeNames.Aux[Domain, Keys]
, getAll0: Mapper.Aux[getAll.type, RepoDomain0, RepoGetAll]
)
: Repos[Domain] = {
val checkLog0 = checkLogPatchBuilder(checks :: loggingAdapters(log) :: patchers.value.instances :: HNil)
val repoDomain = checksToRepoDomain(checkLog0)
val keysList = hlistToList[String](keys())
val repoMap0 = (keysList zip hlistToList[Repo[_]](repoDomain)).toMap
new Repos[Domain] {
override type RepoDomain = RepoDomain0
override def serviceRepos
(
actor: Actor with ActorLogging,
timeout: Timeout,
mat: Materializer,
refLookups: collection.Map[String, RefLookup[_, _]]
): ServiceRepos = new ServiceRepos {
val crudSource = crudFlow(actor, timeout, mat, refLookups)
override def init(initSuccess: Option[() => Unit]) {
dbService.!(Load(keysList: _*))(actor.self)
actor.context.become(initing(initSuccess), false)
}
override def initing(initSuccess: Option[() => Unit]): Receive = {
case Loaded(data, errors) =>
if (errors.nonEmpty) {
log.error("Critical database error. Error loading data during boot.")
bootService.!(InitFailure)(actor.self)
} else {
//TODO make sure data for all types were loaded
keysList.map { t =>
repoMap.get(t).map { repo =>
data.get(t).map { datas =>
val mp = datas.map(a => repo.hasId.id(a.asInstanceOf[repo.Model0]).get -> a)
repo.completes.asInstanceOf[mutable.Map[Long, Any]] ++= mp
repo.completes.keys.toList.sortBy(-_).headOption.foreach(repo.idGen.setAndIncId(_))
log.info("Loaded {} {}", repo.completes.size, t)
}
}
}
initSuccess.map(_ ()).getOrElse {
bootService.!(InitSuccess)(actor.self)
actor.context.unbecome
}
}
}
override def receive() = {
case GetAll =>
log.debug("get all")
actor.sender.!(GetAllReply(getAll0(repos)))(actor.self)
case g: Get[_] =>
crudSource.offer((g, actor.sender))
case (g: Get[_], _) =>
val gr = get(g)
actor.sender.!(gr)(actor.self)
case s: Save[_] =>
crudSource.offer((s, actor.sender))
case (s: Save[_], r: Refs[_]) =>
val sr = save(s, r)
sr match {
case SaveInserted(data, diff, _) =>
cometService.!(CometSend(typeableToSimpleName(s.tt), diff, s.page))(actor.self)
dbService.!(Insert(data))(actor.self)
case SaveUpdated(su, cometSend, _) =>
cometService.!(CometSend(typeableToSimpleName(s.tt), cometSend, s.page))(actor.self)
dbService.!(Upsert(s.t))(actor.self)
case IncompletelyUpdated(_, cometSend, _) =>
cometService.!(CometSend(typeableToSimpleName(s.tt), cometSend, s.page))(actor.self)
case x => log.warning("" +
"sr match error: " + x)
}
actor.sender.!(sr)(actor.self)
case u: Update[_] =>
crudSource.offer((u, actor.sender))
case (u: Update[_], r: Refs[_]) =>
val sr = update(u, r)
sr match {
case SaveInserted(data, diff, _) =>
cometService.!(CometSend(typeableToSimpleName(u.tt), diff, u.page))(actor.self)
dbService.!(Insert(data))(actor.self)
case SaveUpdated(data, diff, _) =>
cometService.!(CometSend(typeableToSimpleName(u.tt), diff, u.page))(actor.self)
dbService.!(Upsert(data))(actor.self)
case IncompletelyUpdated(_, cometSend, _) =>
cometService.!(CometSend(typeableToSimpleName(u.tt), cometSend, u.page))(actor.self)
case x => log.warning("sr match error: " + x)
}
actor.sender.!(sr)(actor.self)
case dbresp@(_: Inserted | _: Upserted) =>
log.debug("Db replied {}", dbresp)
}
}
override def repoMap = repoMap0
override def repos = repoDomain
}
}
}
final class MkRepo[Model] {
def apply
(check: Check[Model])
(implicit
hasId: HasId[Model],
typeable: Typeable[Model],
patcher: patch.Patcher[Model]
): Repo[Model] = new Repo[Model](check)
}
object mkRepo extends Poly1 {
implicit def apply[Model]
(implicit
hasId: HasId[Model],
typeable: Typeable[Model]
): Case.Aux[(Check[Model], LoggingAdapter, patch.Patcher[Model]), Repo[Model]] =
at[(Check[Model], LoggingAdapter, patch.Patcher[Model])] {
case (check, log, patchBuilder) =>
implicit def patchBuilder0 = patchBuilder
Repo[Model](check, log)
}
}
object getAll extends Poly1 {
implicit def apply[Model]
(implicit
typeable: Typeable[Model]
): Case.Aux[Repo[Model], List[Model]] =
at[Repo[Model]] { repo =>
repo.get(Get[repo.Model0]()(typeable.asInstanceOf[Typeable[repo.Model0]])) match {
case GetReplyList(ts) => ts.asInstanceOf[List[Model]]
case _ => Nil
}
}
}
sealed trait TypeNames[Values <: HList] {
type Out <: HList
def apply(): Out
}
object TypeNames {
type Aux[Values <: HList, Out0 <: HList] =
TypeNames[Values] {type Out = Out0}
}
implicit def typeNames0: TypeNames.Aux[HNil, HNil] =
new TypeNames[HNil] {
override type Out = HNil
override def apply = HNil
}
implicit def typeNames1
[HeadValue, TailValues <: HList]
(implicit
tt: Typeable[HeadValue],
tail: TypeNames[TailValues]
)
: TypeNames.Aux[HeadValue :: TailValues, String :: tail.Out] = {
val tn = typeableToSimpleName(tt)
new TypeNames[HeadValue :: TailValues] {
override type Out = String :: tail.Out
override def apply() = tn :: tail()
}
}
def apply[Domain <: HList](
bootService: ActorRef,
dbService: ActorRef,
cometService: ActorRef,
log: LoggingAdapter = NoLogging) =
new MkRepos[Domain](bootService, dbService, cometService, log)
def crudFlow[Model]
(
actor: Actor with ActorLogging,
timeout: Timeout,
mat: Materializer,
refLookups: collection.Map[String, RefLookup[_, _]]
): SourceQueueWithComplete[(Crud, ActorRef)] = {
implicit def to = timeout
implicit def m = mat
implicit def ec = actor.context.dispatcher
Source.queue[(Crud, ActorRef)](10, OverflowStrategy.backpressure).
mapAsync(1) {
case (save: Save[Model], sender) =>
refLookups.get(typeableToSimpleName(save.tt)).map(
_.asInstanceOf[RefLookup[Model, _]](save.t).map((save, _, sender)))
.getOrElse(Future.successful((save, Refs.empty[Model], sender)))
case (update: Update[Model], sender) =>
refLookups.get(typeableToSimpleName(update.tt)).map(
_.asInstanceOf[RefLookup[Model, _]](update.patch).map((update, _, sender)))
.getOrElse(Future.successful((update, Refs.empty[Model], sender)))
case (crud: Crud, sender) => Future.successful((crud, null, sender))
}.
toMat(Sink.foreach { case (save, ref, sender) =>
actor.self.!((save, ref))(sender)
})( Keep.left).run
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy