From c0d574dc6134e4f406875ea5a1301ba46602a6ec Mon Sep 17 00:00:00 2001 From: vlad Date: Fri, 15 Jul 2016 19:41:26 -0400 Subject: Initial commit with standard lib, might be used a example of cake --- src/main/scala/com/drivergrp/core/DriverApp.scala | 75 ++++++++++++ src/main/scala/com/drivergrp/core/Service.scala | 34 ++++++ src/main/scala/com/drivergrp/core/Swagger.scala | 45 +++++++ src/main/scala/com/drivergrp/core/config.scala | 43 +++++++ src/main/scala/com/drivergrp/core/database.scala | 45 +++++++ src/main/scala/com/drivergrp/core/execution.scala | 28 +++++ src/main/scala/com/drivergrp/core/generators.scala | 53 +++++++++ src/main/scala/com/drivergrp/core/id.scala | 24 ++++ src/main/scala/com/drivergrp/core/logging.scala | 132 +++++++++++++++++++++ src/main/scala/com/drivergrp/core/messages.scala | 74 ++++++++++++ src/main/scala/com/drivergrp/core/package.scala | 19 +++ src/main/scala/com/drivergrp/core/rest.scala | 92 ++++++++++++++ src/main/scala/com/drivergrp/core/stats.scala | 50 ++++++++ src/main/scala/com/drivergrp/core/time.scala | 79 ++++++++++++ 14 files changed, 793 insertions(+) create mode 100644 src/main/scala/com/drivergrp/core/DriverApp.scala create mode 100644 src/main/scala/com/drivergrp/core/Service.scala create mode 100644 src/main/scala/com/drivergrp/core/Swagger.scala create mode 100644 src/main/scala/com/drivergrp/core/config.scala create mode 100644 src/main/scala/com/drivergrp/core/database.scala create mode 100644 src/main/scala/com/drivergrp/core/execution.scala create mode 100644 src/main/scala/com/drivergrp/core/generators.scala create mode 100644 src/main/scala/com/drivergrp/core/id.scala create mode 100644 src/main/scala/com/drivergrp/core/logging.scala create mode 100644 src/main/scala/com/drivergrp/core/messages.scala create mode 100644 src/main/scala/com/drivergrp/core/package.scala create mode 100644 src/main/scala/com/drivergrp/core/rest.scala create mode 100644 src/main/scala/com/drivergrp/core/stats.scala create mode 100644 src/main/scala/com/drivergrp/core/time.scala (limited to 'src/main/scala') diff --git a/src/main/scala/com/drivergrp/core/DriverApp.scala b/src/main/scala/com/drivergrp/core/DriverApp.scala new file mode 100644 index 0000000..01fee03 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/DriverApp.scala @@ -0,0 +1,75 @@ +package com.drivergrp.core + +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.RouteResult._ +import akka.stream.ActorMaterializer +import com.drivergrp.core.config.ConfigModule +import com.drivergrp.core.logging.LoggerModule + + +trait DriverApp { + this: ConfigModule with LoggerModule => + + def interface: String = "localhost" + def port: Int = 8080 + + def services: Seq[Service] + + + val servicesInstances = services + activateServices(servicesInstances) + scheduleServicesDeactivation(servicesInstances) + + implicit val actorSystem = ActorSystem("spray-routing", config) + implicit val executionContext = actorSystem.dispatcher + implicit val materializer = ActorMaterializer()(actorSystem) + + val serviceTypes = servicesInstances.flatMap(_.serviceTypes) + val swaggerService = new Swagger(actorSystem, serviceTypes, config) + val swaggerRoutes = swaggerService.routes ~ swaggerService.swaggerUI + + Http()(actorSystem).bindAndHandle( + route2HandlerFlow(logRequestResult("log")(servicesInstances.map(_.route).foldLeft(swaggerRoutes) { _ ~ _ })), + interface, port)(materializer) + + + /** + * Initializes services + */ + protected def activateServices(servicesInstances: Seq[Service]) = { + servicesInstances.foreach { service => + Console.print(s"Service ${service.name} starts ...") + try { + service.activate() + } catch { + case t: Throwable => + log.fatal(s"Service ${service.name} failed to activate", t) + Console.print(" Failed! (check log)") + } + Console.println(" Done") + } + } + + /** + * Schedules services to be deactivated on the app shutdown + */ + protected def scheduleServicesDeactivation(servicesInstances: Seq[Service]) = { + Runtime.getRuntime.addShutdownHook(new Thread() { + override def run(): Unit = { + servicesInstances.foreach { service => + Console.print(s"Service ${service.name} shutting down ...") + try { + service.deactivate() + } catch { + case t: Throwable => + log.fatal(s"Service ${service.name} failed to deactivate", t) + Console.print(" Failed! (check log)") + } + Console.println(" Done") + } + } + }) + } +} diff --git a/src/main/scala/com/drivergrp/core/Service.scala b/src/main/scala/com/drivergrp/core/Service.scala new file mode 100644 index 0000000..4404c9c --- /dev/null +++ b/src/main/scala/com/drivergrp/core/Service.scala @@ -0,0 +1,34 @@ +package com.drivergrp.core + +import akka.http.scaladsl.server.{Route, RouteConcatenation} + + +trait Service { + import scala.reflect.runtime.universe._ + + val name: String + def route: Route + def serviceTypes: Seq[Type] + + def activate(): Unit = {} + def deactivate(): Unit = {} +} + +/** + * Service implementation which may be used to composed a few + * + * @param name more general name of the composite service, + * must be provided as there is no good way to automatically + * generalize the name from the composed services' names + * @param services services to compose into a single one + */ +class CompositeService(val name: String, services: Seq[Service]) + extends Service with RouteConcatenation { + + def route: Route = services.map(_.route).reduce(_ ~ _) + + def serviceTypes = services.flatMap(_.serviceTypes) + + override def activate() = services.foreach(_.activate()) + override def deactivate() = services.reverse.foreach(_.deactivate()) +} \ No newline at end of file diff --git a/src/main/scala/com/drivergrp/core/Swagger.scala b/src/main/scala/com/drivergrp/core/Swagger.scala new file mode 100644 index 0000000..428920c --- /dev/null +++ b/src/main/scala/com/drivergrp/core/Swagger.scala @@ -0,0 +1,45 @@ +package com.drivergrp.core + +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import com.github.swagger.akka.model._ +import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} +import com.typesafe.config.Config + +import scala.reflect.runtime.universe._ + + +class Swagger(override val actorSystem: ActorSystem, + override val apiTypes: Seq[Type], + val config: Config) extends SwaggerHttpService with HasActorSystem { + + val materializer = ActorMaterializer()(actorSystem) + + override val host = "localhost:8080" //the url of your api, not swagger's json endpoint + override val basePath = config.getString("swagger.basePath") + override val apiDocsPath = config.getString("swagger.docsPath") + + override val info = Info( + config.getString("swagger.apiInfo.description"), + config.getString("swagger.apiVersion"), + config.getString("swagger.apiInfo.title"), + config.getString("swagger.apiInfo.termsOfServiceUrl"), + contact = Some(Contact( + config.getString("swagger.apiInfo.contact.name"), + config.getString("swagger.apiInfo.contact.url"), + config.getString("swagger.apiInfo.contact.email") + )), + license = Some(License( + config.getString("swagger.apiInfo.license"), + config.getString("swagger.apiInfo.licenseUrl") + )), + vendorExtensions = Map()) + + def swaggerUI = get { + pathPrefix("") { + pathEndOrSingleSlash { + getFromResource("swagger-ui/index.html") + } + } ~ getFromResourceDirectory("swagger-ui") + } +} \ No newline at end of file diff --git a/src/main/scala/com/drivergrp/core/config.scala b/src/main/scala/com/drivergrp/core/config.scala new file mode 100644 index 0000000..5a89752 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/config.scala @@ -0,0 +1,43 @@ +package com.drivergrp.core + +import java.io.File +import com.typesafe.config.{Config, ConfigFactory} + + +object config { + + trait ConfigModule { + def config: Config + } + + /** + * Configuration implementation providing config which is specified as the parameter + * which might be used for testing purposes + * + * @param config fixed config to provide + */ + class DefaultConfigModule(val config: Config) extends ConfigModule + + /** + * Configuration implementation reading default typesafe config + */ + trait TypesafeConfigModule extends ConfigModule { + + private val internalConfig: Config = { + val configDefaults = + ConfigFactory.load(this.getClass.getClassLoader, "application.conf") + + scala.sys.props.get("application.config") match { + + case Some(filename) => + ConfigFactory.parseFile(new File(filename)).withFallback(configDefaults) + + case None => configDefaults + } + } + + protected val rootConfig = internalConfig + + val config = rootConfig + } +} diff --git a/src/main/scala/com/drivergrp/core/database.scala b/src/main/scala/com/drivergrp/core/database.scala new file mode 100644 index 0000000..5eb9d28 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/database.scala @@ -0,0 +1,45 @@ +package com.drivergrp.core + +import com.drivergrp.core.id.{Id, Name} + +import scala.concurrent.Future + + +object database { + + import slick.backend.DatabaseConfig + import slick.driver.JdbcProfile + + + trait DatabaseModule { + val profile: JdbcProfile + val database: JdbcProfile#Backend#Database + } + + trait ConfigDatabaseModule extends DatabaseModule { + + protected def databaseConfigKey: String + + private val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseConfigKey) + + val profile: JdbcProfile = dbConfig.driver + val database: JdbcProfile#Backend#Database = dbConfig.db + } + + trait DatabaseObject { + def createTables(): Future[Unit] + def disconnect(): Unit + } + + trait IdColumnTypes { + this: DatabaseModule => + + import profile.api._ + + implicit def idColumnType[T] = + MappedColumnType.base[Id[T], Long]({ id => id: Long }, { id => Id[T](id) }) + + implicit def nameColumnType[T] = + MappedColumnType.base[Name[T], String]({ name => name: String }, { name => Name[T](name) }) + } +} diff --git a/src/main/scala/com/drivergrp/core/execution.scala b/src/main/scala/com/drivergrp/core/execution.scala new file mode 100644 index 0000000..7274f00 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/execution.scala @@ -0,0 +1,28 @@ +package com.drivergrp.core + + +object execution { + + import scala.concurrent.ExecutionContext + import java.util.concurrent.Executors + import akka.actor.ActorSystem + + + trait ExecutionContextModule { + + def executionContext: ExecutionContext + } + + trait FixedThreadsExecutionContext extends ExecutionContextModule { + + def threadsNumber: Int + + val executionContext: ExecutionContext = + ExecutionContext.fromExecutor(Executors.newFixedThreadPool(threadsNumber)) + } + + trait ActorSystemModule { + + def actorSystem: ActorSystem + } +} diff --git a/src/main/scala/com/drivergrp/core/generators.scala b/src/main/scala/com/drivergrp/core/generators.scala new file mode 100644 index 0000000..89d6e56 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/generators.scala @@ -0,0 +1,53 @@ +package com.drivergrp.core + +import java.math.MathContext + +import com.drivergrp.core.id.{Id, Name} +import com.drivergrp.core.time.{Time, TimeRange} + +import scala.reflect.ClassTag +import scala.util.Random + + +object generators { + + private val random = new Random + import random._ + + private val DefaultMaxLength = 100 + + + def nextId[T](): Id[T] = Id[T](nextLong()) + + def nextName[T](maxLength: Int = DefaultMaxLength): Name[T] = Name[T](nextString(maxLength)) + + def nextString(maxLength: Int = DefaultMaxLength) = random.nextString(maxLength) + + def nextOption[T](value: => T): Option[T] = if(nextBoolean) Option(value) else None + + def nextPair[L, R](left: => L, right: => R): (L, R) = (left, right) + + def nextTriad[F, S, T](first: => F, second: => S, third: => T): (F, S, T) = (first, second, third) + + def nextTime(): Time = Time(math.abs(nextLong() % System.currentTimeMillis)) + + def nextTimeRange(): TimeRange = TimeRange(nextTime(), nextTime()) + + def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = + BigDecimal(multiplier * nextDouble, new MathContext(precision)) + + def oneOf[T](items: Seq[T]): T = items(nextInt(items.size)) + + def arrayOf[T : ClassTag](generator: => T, maxLength: Int = DefaultMaxLength): Array[T] = Array.fill(maxLength)(generator) + + def seqOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Seq[T] = Seq.fill(maxLength)(generator) + + def vectorOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Vector[T] = Vector.fill(maxLength)(generator) + + def listOf[T](generator: => T, maxLength: Int = DefaultMaxLength): List[T] = List.fill(maxLength)(generator) + + def setOf[T](generator: => T, maxLength: Int = DefaultMaxLength): Set[T] = seqOf(generator, maxLength).toSet + + def mapOf[K, V](maxLength: Int, keyGenerator: => K, valueGenerator: => V): Map[K, V] = + seqOf(nextPair(keyGenerator, valueGenerator), maxLength).toMap +} \ No newline at end of file diff --git a/src/main/scala/com/drivergrp/core/id.scala b/src/main/scala/com/drivergrp/core/id.scala new file mode 100644 index 0000000..29b8d99 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/id.scala @@ -0,0 +1,24 @@ +package com.drivergrp.core + +import scalaz._ + + +object id { + + trait Tagged[+V, +Tag] + type @@[+V, +Tag] = V with Tagged[V, Tag] + + type Id[+Tag] = Long @@ Tag + object Id { + def apply[Tag](value: Long) = value.asInstanceOf[Id[Tag]] + } + + implicit def idEqual[T]: Equal[Id[T]] = Equal.equal[Id[T]](_ == _) + + type Name[+Tag] = String @@ Tag + object Name { + def apply[Tag](value: String) = value.asInstanceOf[Name[Tag]] + } + + implicit def nameEqual[T]: Equal[Name[T]] = Equal.equal[Name[T]](_ == _) +} diff --git a/src/main/scala/com/drivergrp/core/logging.scala b/src/main/scala/com/drivergrp/core/logging.scala new file mode 100644 index 0000000..35f3a1b --- /dev/null +++ b/src/main/scala/com/drivergrp/core/logging.scala @@ -0,0 +1,132 @@ +package com.drivergrp.core + +import com.typesafe.scalalogging.{LazyLogging, StrictLogging} +import org.slf4j.Marker + + +object logging { + + trait LoggerModule { + + def log: Logger + } + + trait NopLoggerModule extends LoggerModule { + + def log: Logger = new NopLogger() + } + + /** + * Defines `logger` as a lazy value initialized with an underlying `org.slf4j.Logger` + * named according to the class into which this trait is mixed. + */ + trait LazyLoggerModule extends LoggerModule { + this: LazyLogging => + + override val log: Logger = new TypesafeScalaLogger(logger) + } + + /** + * Defines `logger` as a value initialized with an underlying `org.slf4j.Logger` + * named according to the class into which this trait is mixed. + */ + trait StrictLoggerModule extends LoggerModule { + this: StrictLogging => + + override val log: Logger = new TypesafeScalaLogger(logger) + } + + + trait Logger { + + def fatal(message: String): Unit + def fatal(message: String, cause: Throwable): Unit + def fatal(message: String, args: AnyRef*): Unit + def fatal(marker: Marker, message: String): Unit + def fatal(marker: Marker, message: String, cause: Throwable): Unit + def fatal(marker: Marker, message: String, args: AnyRef*): Unit + + def error(message: String): Unit + def error(message: String, cause: Throwable): Unit + def error(message: String, args: AnyRef*): Unit + def error(marker: Marker, message: String): Unit + def error(marker: Marker, message: String, cause: Throwable): Unit + def error(marker: Marker, message: String, args: AnyRef*): Unit + + def audit(message: String): Unit + def audit(message: String, cause: Throwable): Unit + def audit(message: String, args: AnyRef*): Unit + def audit(marker: Marker, message: String): Unit + def audit(marker: Marker, message: String, cause: Throwable): Unit + def audit(marker: Marker, message: String, args: AnyRef*): Unit + + def debug(message: String): Unit + def debug(message: String, cause: Throwable): Unit + def debug(message: String, args: AnyRef*): Unit + def debug(marker: Marker, message: String): Unit + def debug(marker: Marker, message: String, cause: Throwable): Unit + def debug(marker: Marker, message: String, args: AnyRef*): Unit + } + + class TypesafeScalaLogger(scalaLogging: com.typesafe.scalalogging.Logger) extends Logger { + + def fatal(message: String): Unit = scalaLogging.error(message) + def fatal(message: String, cause: Throwable): Unit = scalaLogging.error(message, cause) + def fatal(message: String, args: AnyRef*): Unit = scalaLogging.error(message, args) + def fatal(marker: Marker, message: String): Unit = scalaLogging.error(marker, message) + def fatal(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.error(marker, message, cause) + def fatal(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.error(marker, message, args) + + def error(message: String): Unit = scalaLogging.warn(message) + def error(message: String, cause: Throwable): Unit = scalaLogging.warn(message, cause) + def error(message: String, args: AnyRef*): Unit = scalaLogging.warn(message, args) + def error(marker: Marker, message: String): Unit = scalaLogging.warn(marker, message) + def error(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.warn(marker, message, cause) + def error(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.warn(marker, message, args) + + def audit(message: String): Unit = scalaLogging.info(message) + def audit(message: String, cause: Throwable): Unit = scalaLogging.info(message, cause) + def audit(message: String, args: AnyRef*): Unit = scalaLogging.info(message, args) + def audit(marker: Marker, message: String): Unit = scalaLogging.info(marker, message) + def audit(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.info(marker, message, cause) + def audit(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.info(marker, message, args) + + def debug(message: String): Unit = scalaLogging.debug(message) + def debug(message: String, cause: Throwable): Unit = scalaLogging.debug(message, cause) + def debug(message: String, args: AnyRef*): Unit = scalaLogging.debug(message, args) + def debug(marker: Marker, message: String): Unit = scalaLogging.debug(marker, message) + def debug(marker: Marker, message: String, cause: Throwable): Unit = scalaLogging.debug(marker, message, cause) + def debug(marker: Marker, message: String, args: AnyRef*): Unit = scalaLogging.debug(marker, message, args) + } + + class NopLogger() extends Logger { + + def fatal(message: String): Unit = {} + def fatal(message: String, cause: Throwable): Unit = {} + def fatal(message: String, args: AnyRef*): Unit = {} + def fatal(marker: Marker, message: String): Unit = {} + def fatal(marker: Marker, message: String, cause: Throwable): Unit = {} + def fatal(marker: Marker, message: String, args: AnyRef*): Unit = {} + + def error(message: String): Unit = {} + def error(message: String, cause: Throwable): Unit = {} + def error(message: String, args: AnyRef*): Unit = {} + def error(marker: Marker, message: String): Unit = {} + def error(marker: Marker, message: String, cause: Throwable): Unit = {} + def error(marker: Marker, message: String, args: AnyRef*): Unit = {} + + def audit(message: String): Unit = {} + def audit(message: String, cause: Throwable): Unit = {} + def audit(message: String, args: AnyRef*): Unit = {} + def audit(marker: Marker, message: String): Unit = {} + def audit(marker: Marker, message: String, cause: Throwable): Unit = {} + def audit(marker: Marker, message: String, args: AnyRef*): Unit = {} + + def debug(message: String): Unit = {} + def debug(message: String, cause: Throwable): Unit = {} + def debug(message: String, args: AnyRef*): Unit = {} + def debug(marker: Marker, message: String): Unit = {} + def debug(marker: Marker, message: String, cause: Throwable): Unit = {} + def debug(marker: Marker, message: String, args: AnyRef*): Unit = {} + } +} diff --git a/src/main/scala/com/drivergrp/core/messages.scala b/src/main/scala/com/drivergrp/core/messages.scala new file mode 100644 index 0000000..cf0bb6c --- /dev/null +++ b/src/main/scala/com/drivergrp/core/messages.scala @@ -0,0 +1,74 @@ +package com.drivergrp.core + + +import java.util.Locale + +import com.drivergrp.core.config.ConfigModule +import com.drivergrp.core.logging.{Logger, LoggerModule} + +import scala.collection.JavaConverters._ +import scala.collection.concurrent.TrieMap + +/** + * Scala internationalization (i18n) support + */ +object messages { + + trait MessagesModule { + + def messages: Messages + } + + trait ConfigMessagesModule extends MessagesModule { + this: ConfigModule with LoggerModule => + + private val loadedFromConfig = new TrieMap[Locale, Messages]() + val locale: Locale = Locale.US + + val messages: Messages = { + loadedFromConfig.getOrElseUpdate(locale, { + val map = config.getConfig(locale.getISO3Language).root().unwrapped().asScala.mapValues(_.toString).toMap + Messages(map, locale, log) + }) + } + } + + + case class Messages(map: Map[String, String], locale: Locale, log: Logger) { + + /** + * Returns message for the key + * + * @param key key + * @return message + */ + def apply(key: String): String = { + map.get(key) match { + case Some(message) => message + case None => + log.error(s"Message with key $key not found for locale " + locale.getDisplayName) + key + } + } + + /** + * Returns message for the key and formats that with parameters + * + * @example "Hello {0}!" with "Joe" will be "Hello Joe!" + * + * @param key key + * @param params params to be embedded + * @return formatted message + */ + def apply(key: String, params: Any*): String = { + + def format(formatString: String, params: Seq[Any]) = + params.zipWithIndex.foldLeft(formatString) { + case (res, (value, index)) => res.replaceAll(s"{$index}", value.toString) + } + + val template = apply(key) + format(template, params) + } + } +} \ No newline at end of file diff --git a/src/main/scala/com/drivergrp/core/package.scala b/src/main/scala/com/drivergrp/core/package.scala new file mode 100644 index 0000000..3c19431 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/package.scala @@ -0,0 +1,19 @@ +package com.drivergrp + + +package object core { + import scala.language.reflectiveCalls + + def make[T](v: => T)(f: T => Unit): T = { + val value = v; f(value); value + } + + def using[R <: { def close() }, P](r: => R)(f: R => P): P = { + val resource = r + try { + f(resource) + } finally { + resource.close() + } + } +} diff --git a/src/main/scala/com/drivergrp/core/rest.scala b/src/main/scala/com/drivergrp/core/rest.scala new file mode 100644 index 0000000..e121640 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/rest.scala @@ -0,0 +1,92 @@ +package com.drivergrp.core + +import akka.http.scaladsl.Http +import akka.http.scaladsl.model.{HttpRequest, HttpResponse} +import akka.http.scaladsl.server.{Directive, _} +import akka.http.scaladsl.util.FastFuture._ +import akka.stream.ActorMaterializer +import akka.util.Timeout +import com.drivergrp.core.execution.{ActorSystemModule, ExecutionContextModule} +import com.drivergrp.core.id.{Id, Name} +import com.drivergrp.core.logging.LoggerModule +import com.drivergrp.core.stats.StatsModule +import com.drivergrp.core.time.TimeRange +import com.drivergrp.core.time.provider.TimeModule +import spray.json.{DeserializationException, JsNumber, JsString, JsValue, RootJsonFormat} + +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.language.postfixOps +import scala.util.{Failure, Success, Try} +import scalaz.{Failure => _, Success => _, _} + + +object rest { + + trait RestService { + this: ActorSystemModule with LoggerModule with StatsModule with TimeModule with ExecutionContextModule => + + protected implicit val timeout = Timeout(5 seconds) + + implicit val materializer = ActorMaterializer()(actorSystem) + + + def sendRequest(request: HttpRequest): Future[HttpResponse] = { + + log.audit(s"Sending to ${request.uri} request $request") + + val requestTime = time.currentTime() + val response = Http()(actorSystem).singleRequest(request)(materializer) + + response.onComplete { + case Success(_) => + val responseTime = time.currentTime() + log.audit(s"Response from ${request.uri} to request $request is successful") + stats.recordStats(Seq("request", request.uri.toString, "success"), TimeRange(requestTime, responseTime), 1) + + case Failure(t) => + val responseTime = time.currentTime() + log.audit(s"Failed to receive response from ${request.uri} to request $request") + log.error(s"Failed to receive response from ${request.uri} to request $request", t) + stats.recordStats(Seq("request", request.uri.toString, "fail"), TimeRange(requestTime, responseTime), 1) + } (executionContext) + + response + } + } + + + object basicFormats { + + implicit def idFormat[T] = new RootJsonFormat[Id[T]] { + def write(id: Id[T]) = JsNumber(id) + + def read(value: JsValue) = value match { + case JsNumber(id) => Id[T](id.toLong) + case _ => throw new DeserializationException("Id expects number") + } + } + + implicit def nameFormat[T] = new RootJsonFormat[Name[T]] { + def write(name: Name[T]) = JsNumber(name) + + def read(value: JsValue): Name[T] = value match { + case JsString(name) => Name[T](name) + case _ => throw new DeserializationException("Name expects string") + } + } + } + + trait OptionTDirectives { + + /** + * "Unwraps" a `OptionT[Future, T]` and runs the inner route after future + * completion with the future's value as an extraction of type `Try[T]`. + */ + def onComplete[T](optionT: OptionT[Future, T]): Directive1[Try[Option[T]]] = + Directive { inner ⇒ ctx ⇒ + import ctx.executionContext + optionT.run.fast.transformWith(t ⇒ inner(Tuple1(t))(ctx)) + } + } +} diff --git a/src/main/scala/com/drivergrp/core/stats.scala b/src/main/scala/com/drivergrp/core/stats.scala new file mode 100644 index 0000000..2a173df --- /dev/null +++ b/src/main/scala/com/drivergrp/core/stats.scala @@ -0,0 +1,50 @@ +package com.drivergrp.core + +import com.drivergrp.core.logging.LoggerModule +import com.drivergrp.core.time.{Time, TimeRange} + +object stats { + + type StatsKey = String + type StatsKeys = Seq[StatsKey] + + + trait StatsModule { + + def stats: Stats + } + + trait Stats { + + def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit + + def recordStats(keys: StatsKeys, interval: TimeRange, value: Int): Unit = + recordStats(keys, interval, BigDecimal(value)) + + def recordStats(key: StatsKey, interval: TimeRange, value: BigDecimal): Unit = + recordStats(Vector(key), interval, value) + + def recordStats(key: StatsKey, interval: TimeRange, value: Int): Unit = + recordStats(Vector(key), interval, BigDecimal(value)) + + def recordStats(keys: StatsKeys, time: Time, value: BigDecimal): Unit = + recordStats(keys, TimeRange(time, time), value) + + def recordStats(keys: StatsKeys, time: Time, value: Int): Unit = + recordStats(keys, TimeRange(time, time), BigDecimal(value)) + + def recordStats(key: StatsKey, time: Time, value: BigDecimal): Unit = + recordStats(Vector(key), TimeRange(time, time), value) + + def recordStats(key: StatsKey, time: Time, value: Int): Unit = + recordStats(Vector(key), TimeRange(time, time), BigDecimal(value)) + } + + trait LogStats extends Stats { + this: LoggerModule => + + def recordStats(keys: StatsKeys, interval: TimeRange, value: BigDecimal): Unit = { + log.audit(s"${keys.mkString(".")}(${interval.start.millis}-${interval.end.millis})=${value.toString}") + } + } +} diff --git a/src/main/scala/com/drivergrp/core/time.scala b/src/main/scala/com/drivergrp/core/time.scala new file mode 100644 index 0000000..645c991 --- /dev/null +++ b/src/main/scala/com/drivergrp/core/time.scala @@ -0,0 +1,79 @@ +package com.drivergrp.core + +import java.text.SimpleDateFormat +import java.util.{Calendar, Date, GregorianCalendar} + +import scala.concurrent.duration.Duration + +object time { + + // The most useful time units + val Second = 1000L + val Seconds = Second + val Minute = 60 * Seconds + val Minutes = Minute + val Hour = 60 * Minutes + val Hours = Hour + val Day = 24 * Hours + val Days = Day + val Week = 7 * Days + val Weeks = Week + + + case class Time(millis: Long) extends AnyVal { + + def isBefore(anotherTime: Time): Boolean = millis < anotherTime.millis + + def isAfter(anotherTime: Time): Boolean = millis > anotherTime.millis + + def advanceBy(duration: Duration): Time = Time(millis + duration.length) + } + + case class TimeRange(start: Time, end: Time) + + implicit def timeOrdering: Ordering[Time] = Ordering.by(_.millis) + + + def startOfMonth(time: Time) = { + make(new GregorianCalendar()) { cal => + cal.setTime(new Date(time.millis)) + cal.set(Calendar.DAY_OF_MONTH, cal.getActualMinimum(Calendar.DAY_OF_MONTH)) + Time(cal.getTime.getTime) + } + } + + def textualDate(time: Time): String = + new SimpleDateFormat("MMMM d, yyyy").format(new Date(time.millis)) + + def textualTime(time: Time): String = + new SimpleDateFormat("MMM dd, yyyy hh:mm:ss a").format(new Date(time.millis)) + + + object provider { + + /** + * Time providers are supplying code with current times + * and are extremely useful for testing to check how system is going + * to behave at specific moments in time. + * + * All the calls to receive current time must be made using time + * provider injected to the caller. + */ + + trait TimeModule { + def time: TimeProvider + } + + trait TimeProvider { + def currentTime(): Time + } + + final class SystemTimeProvider extends TimeProvider { + def currentTime() = Time(System.currentTimeMillis()) + } + + final class SpecificTimeProvider(time: Time) extends TimeProvider { + def currentTime() = time + } + } +} -- cgit v1.2.3