All Downloads are FREE. Search and download functionalities are using the official Maven repository.

no.kodeworks.kvarg.repo.Repos.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
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